diff --git a/CHANGELOG.md b/CHANGELOG.md index ef4b16a4..d9e0af4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Add `assert_have_been_called_nth_with` for verifying arguments on the Nth invocation of a spy - Add `assert_string_matches_format` and `assert_string_not_matches_format` with format placeholders (`%d`, `%s`, `%f`, `%i`, `%x`, `%e`, `%%`) - Add JSON assertions: `assert_json_key_exists`, `assert_json_contains`, `assert_json_equals` (requires `jq`) +- Add duration assertions: `assert_duration`, `assert_duration_less_than`, `assert_duration_greater_than` ### Changed - Split Windows CI test jobs into parallel chunks to avoid timeouts diff --git a/docs/assertions.md b/docs/assertions.md index 55b4c54b..b37cbfac 100644 --- a/docs/assertions.md +++ b/docs/assertions.md @@ -1270,6 +1270,57 @@ function test_failure() { ``` ::: +## assert_duration +> `assert_duration "command" threshold_ms` + +Reports an error if `command` takes longer than `threshold_ms` milliseconds to execute. Uses the framework's portable clock internally. + +::: code-group +```bash [Example] +function test_success() { + assert_duration "echo hello" 500 +} + +function test_failure() { + assert_duration "sleep 2" 1000 +} +``` +::: + +## assert_duration_less_than +> `assert_duration_less_than "command" threshold_ms` + +Reports an error if `command` takes `threshold_ms` milliseconds or more to execute. Stricter than [assert_duration](#assert-duration) which allows equal values. + +::: code-group +```bash [Example] +function test_success() { + assert_duration_less_than "echo hello" 500 +} + +function test_failure() { + assert_duration_less_than "sleep 2" 1000 +} +``` +::: + +## assert_duration_greater_than +> `assert_duration_greater_than "command" threshold_ms` + +Reports an error if `command` completes in `threshold_ms` milliseconds or less. Useful for verifying that a command takes at least a minimum amount of time. + +::: code-group +```bash [Example] +function test_success() { + assert_duration_greater_than "sleep 1" 500 +} + +function test_failure() { + assert_duration_greater_than "echo hello" 5000 +} +``` +::: + ## bashunit::fail > `bashunit::fail "failure message"` diff --git a/src/assert_duration.sh b/src/assert_duration.sh new file mode 100644 index 00000000..7e33f0b9 --- /dev/null +++ b/src/assert_duration.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash + +function bashunit::duration::measure_ms() { + local command="$1" + + local start_ns + start_ns=$(bashunit::clock::now) + + eval "$command" >/dev/null 2>&1 + + local end_ns + end_ns=$(bashunit::clock::now) + + local elapsed_ms + elapsed_ms=$(bashunit::math::calculate "($end_ns - $start_ns) / 1000000" | awk '{printf "%.0f", $1}') + + echo "$elapsed_ms" +} + +function assert_duration() { + bashunit::assert::should_skip && return 0 + + local command="$1" + local threshold_ms="$2" + + local elapsed_ms + elapsed_ms=$(bashunit::duration::measure_ms "$command") + + if [ "$elapsed_ms" -gt "$threshold_ms" ]; then + local test_fn + test_fn="$(bashunit::helper::find_test_function_name)" + local label + label="$(bashunit::helper::normalize_test_function_name "$test_fn")" + bashunit::assert::mark_failed + bashunit::console_results::print_failed_test "${label}" "${threshold_ms}" "to complete within (ms)" "${command}" + return + fi + + bashunit::state::add_assertions_passed +} + +function assert_duration_less_than() { + bashunit::assert::should_skip && return 0 + + local command="$1" + local threshold_ms="$2" + + local elapsed_ms + elapsed_ms=$(bashunit::duration::measure_ms "$command") + + if [ "$elapsed_ms" -ge "$threshold_ms" ]; then + local test_fn + test_fn="$(bashunit::helper::find_test_function_name)" + local label + label="$(bashunit::helper::normalize_test_function_name "$test_fn")" + bashunit::assert::mark_failed + bashunit::console_results::print_failed_test "${label}" "${threshold_ms}" "to complete within (ms)" "${command}" + return + fi + + bashunit::state::add_assertions_passed +} + +function assert_duration_greater_than() { + bashunit::assert::should_skip && return 0 + + local command="$1" + local threshold_ms="$2" + + local elapsed_ms + elapsed_ms=$(bashunit::duration::measure_ms "$command") + + if [ "$elapsed_ms" -le "$threshold_ms" ]; then + local test_fn + test_fn="$(bashunit::helper::find_test_function_name)" + local label + label="$(bashunit::helper::normalize_test_function_name "$test_fn")" + bashunit::assert::mark_failed + bashunit::console_results::print_failed_test "${label}" "${threshold_ms}" "to take at least (ms)" "${command}" + return + fi + + bashunit::state::add_assertions_passed +} diff --git a/src/assertions.sh b/src/assertions.sh index c612afb8..2cd50ee1 100644 --- a/src/assertions.sh +++ b/src/assertions.sh @@ -2,6 +2,7 @@ source "$BASHUNIT_ROOT_DIR/src/assert.sh" source "$BASHUNIT_ROOT_DIR/src/assert_arrays.sh" +source "$BASHUNIT_ROOT_DIR/src/assert_duration.sh" source "$BASHUNIT_ROOT_DIR/src/assert_files.sh" source "$BASHUNIT_ROOT_DIR/src/assert_folders.sh" source "$BASHUNIT_ROOT_DIR/src/assert_json.sh" diff --git a/tests/unit/assert_duration_test.sh b/tests/unit/assert_duration_test.sh new file mode 100644 index 00000000..61b3e609 --- /dev/null +++ b/tests/unit/assert_duration_test.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2329 + +function test_successful_assert_duration_within() { + assert_empty "$(assert_duration "sleep 0" 1000)" +} + +function test_successful_assert_duration_within_fast_command() { + assert_empty "$(assert_duration "echo hello" 500)" +} + +function test_unsuccessful_assert_duration_exceeds_threshold() { + assert_same \ + "$(bashunit::console_results::print_failed_test \ + "Unsuccessful assert duration exceeds threshold" \ + "1000" "to complete within (ms)" "sleep 1")" \ + "$(assert_duration "sleep 1" 1000)" +} + +function test_successful_assert_duration_less_than() { + assert_empty "$(assert_duration_less_than "sleep 0" 1000)" +} + +function test_unsuccessful_assert_duration_less_than() { + assert_same \ + "$(bashunit::console_results::print_failed_test \ + "Unsuccessful assert duration less than" \ + "100" "to complete within (ms)" "sleep 1")" \ + "$(assert_duration_less_than "sleep 1" 100)" +} + +function test_successful_assert_duration_greater_than() { + assert_empty "$(assert_duration_greater_than "sleep 1" 500)" +} + +function test_unsuccessful_assert_duration_greater_than() { + assert_same \ + "$(bashunit::console_results::print_failed_test \ + "Unsuccessful assert duration greater than" \ + "5000" "to take at least (ms)" "echo hello")" \ + "$(assert_duration_greater_than "echo hello" 5000)" +}