diff --git a/CHANGELOG.md b/CHANGELOG.md index f04135b2..556c0196 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unreleased ### Added +- `assert_within_delta ` asserts a number is within a tolerance of another; supports floats (#744) - `assert_array_length ` asserts an array has exactly `n` elements (#743) - `assert_file_permissions ` asserts a file's octal permission mode (`644`, `0755`); portable across GNU/BSD `stat` (#742) diff --git a/docs/assertions.md b/docs/assertions.md index 81212065..592d0358 100644 --- a/docs/assertions.md +++ b/docs/assertions.md @@ -361,6 +361,25 @@ function test_failure() { ``` ::: +## assert_within_delta +> `assert_within_delta "expected" "actual" "delta"` + +Reports an error if `actual` is not within `delta` of `expected` +(i.e. `|actual - expected| > delta`). Supports floating-point values. +Useful for timing or measured values where exact equality is too strict. + +::: code-group +```bash [Example] +function test_success() { + assert_within_delta "3.14159" "3.14" "0.01" +} + +function test_failure() { + assert_within_delta "100" "105" "3" +} +``` +::: + ## assert_date_equals > `assert_date_equals "expected" "actual"` diff --git a/src/assert.sh b/src/assert.sh index 1a23a02a..1126e6d3 100755 --- a/src/assert.sh +++ b/src/assert.sh @@ -792,6 +792,61 @@ function assert_greater_or_equal_than() { bashunit::state::add_assertions_passed } +## +# Whether a value looks like a number (integer or decimal, optional sign). +# Returns: 0 when numeric, 1 otherwise. +## +function bashunit::assert::_is_numeric() { + local value="$1" + case "$value" in + '' | *[!0-9.+-]*) return 1 ;; + esac + # Must contain at least one digit (rejects ".", "-", "+"). + case "$value" in + *[0-9]*) return 0 ;; + esac + return 1 +} + +## +# Asserts the actual value is within +/- delta of the expected value: +# |actual - expected| <= delta. Supports floats via bashunit::math::calculate. +# Arguments: $1 - expected, $2 - actual, $3 - delta +## +function assert_within_delta() { + bashunit::assert::should_skip && return 0 + + local expected="$1" + local actual="$2" + local delta="$3" + local label + label="$(bashunit::assert::label)" + + if ! bashunit::assert::_is_numeric "$expected" || + ! bashunit::assert::_is_numeric "$actual" || + ! bashunit::assert::_is_numeric "$delta"; then + bashunit::assert::mark_failed + bashunit::console_results::print_failed_test \ + "${label}" "${expected} ${actual} ${delta}" "to all be numeric" "but got a non-numeric value" + return + fi + + local diff + diff="$(bashunit::math::calculate "$expected - $actual")" + case "$diff" in + -*) diff="${diff#-}" ;; + esac + + if [ "$(bashunit::math::calculate "$diff <= $delta")" != "1" ]; then + bashunit::assert::mark_failed + bashunit::console_results::print_failed_test \ + "${label}" "${actual}" "to be within ${delta} of" "${expected}" + return + fi + + bashunit::state::add_assertions_passed +} + function assert_line_count() { bashunit::assert::should_skip && return 0 local IFS=$' \t\n' diff --git a/tests/acceptance/snapshots/bashunit_test_sh.test_bashunit_should_display_all_assert_docs.snapshot b/tests/acceptance/snapshots/bashunit_test_sh.test_bashunit_should_display_all_assert_docs.snapshot index 9e0977ec..aaf7b4a3 100644 --- a/tests/acceptance/snapshots/bashunit_test_sh.test_bashunit_should_display_all_assert_docs.snapshot +++ b/tests/acceptance/snapshots/bashunit_test_sh.test_bashunit_should_display_all_assert_docs.snapshot @@ -150,6 +150,15 @@ Reports an error if `actual` is not greater than or equal to `expected`. - assert_less_or_equal_than is the inverse of this assertion and takes the same arguments. +## assert_within_delta +-------------- +> `assert_within_delta "expected" "actual" "delta"` + +Reports an error if `actual` is not within `delta` of `expected` +(i.e. `|actual - expected| > delta`). Supports floating-point values. +Useful for timing or measured values where exact equality is too strict. + + ## assert_date_equals -------------- > `assert_date_equals "expected" "actual"` diff --git a/tests/unit/assert_numeric_test.sh b/tests/unit/assert_numeric_test.sh index 6206f75e..d8f7020d 100644 --- a/tests/unit/assert_numeric_test.sh +++ b/tests/unit/assert_numeric_test.sh @@ -125,3 +125,30 @@ function test_unsuccessful_assert_greater_or_equal_than() { "Unsuccessful assert greater or equal than" "1" "to be greater or equal than" "3")" \ "$(assert_greater_or_equal_than "3" "1")" } + +function test_successful_assert_within_delta() { + assert_empty "$(assert_within_delta "3.14159" "3.14" "0.01")" +} + +function test_successful_assert_within_delta_with_a_negative_difference() { + assert_empty "$(assert_within_delta "100" "105" "10")" +} + +function test_successful_assert_within_delta_when_equal() { + assert_empty "$(assert_within_delta "42" "42" "0")" +} + +function test_unsuccessful_assert_within_delta() { + assert_same \ + "$(bashunit::console_results::print_failed_test \ + "Unsuccessful assert within delta" "105" "to be within 3 of" "100")" \ + "$(assert_within_delta "100" "105" "3")" +} + +function test_unsuccessful_assert_within_delta_with_a_non_numeric_value() { + assert_same \ + "$(bashunit::console_results::print_failed_test \ + "Unsuccessful assert within delta with a non numeric value" \ + "abc 105 3" "to all be numeric" "but got a non-numeric value")" \ + "$(assert_within_delta "abc" "105" "3")" +}