diff --git a/CHANGELOG.md b/CHANGELOG.md index bd40185e..0d33c4ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,11 @@ ## Unreleased ### Added +- Add date comparison assertions: `assert_date_equals`, `assert_date_before`, `assert_date_after`, `assert_date_within_range`, `assert_date_within_delta` + - Accepts epoch seconds (integers) or ISO 8601 dates (`2023-11-14`, `2023-11-14T12:00:00`) + - Auto-detects format and converts ISO 8601 to epoch via `bashunit::date::to_epoch` + - Mixed formats supported (one epoch, one ISO) in the same assertion call + - Fully portable across GNU and BSD systems - Add Claude Code configuration with custom skills, agents, and rules - Custom skills for TDD workflow, test fixes, assertions, coverage, and releases - Expert agents for Bash 3.2+ compatibility, code review, TDD coaching, test architecture, and performance diff --git a/docs/assertions.md b/docs/assertions.md index 6631ab32..7e931ae0 100644 --- a/docs/assertions.md +++ b/docs/assertions.md @@ -326,6 +326,108 @@ function test_failure() { ``` ::: +## assert_date_equals +> `assert_date_equals "expected" "actual"` + +Reports an error if the two epoch timestamps `expected` and `actual` are not equal. + +All inputs are **epoch seconds** (integers), generated via `date +%s`. + +::: code-group +```bash [Example] +function test_success() { + local now + now="$(date +%s)" + + assert_date_equals "$now" "$now" +} + +function test_failure() { + assert_date_equals "1700000000" "1600000000" +} +``` +::: + +## assert_date_before +> `assert_date_before "expected" "actual"` + +Reports an error if `actual` is not before `expected` (i.e. `actual` must be less than `expected`). + +All inputs are **epoch seconds** (integers), generated via `date +%s`. + +::: code-group +```bash [Example] +function test_success() { + assert_date_before "1700000000" "1600000000" +} + +function test_failure() { + assert_date_before "1700000000" "1800000000" +} +``` +::: + +## assert_date_after +> `assert_date_after "expected" "actual"` + +Reports an error if `actual` is not after `expected` (i.e. `actual` must be greater than `expected`). + +All inputs are **epoch seconds** (integers), generated via `date +%s`. + +::: code-group +```bash [Example] +function test_success() { + assert_date_after "1600000000" "1700000000" +} + +function test_failure() { + assert_date_after "1600000000" "1500000000" +} +``` +::: + +## assert_date_within_range +> `assert_date_within_range "from" "to" "actual"` + +Reports an error if `actual` does not fall between `from` and `to` (inclusive). + +All inputs are **epoch seconds** (integers), generated via `date +%s`. + +::: code-group +```bash [Example] +function test_success() { + assert_date_within_range "1600000000" "1800000000" "1700000000" +} + +function test_failure() { + assert_date_within_range "1600000000" "1800000000" "1900000000" +} +``` +::: + +## assert_date_within_delta +> `assert_date_within_delta "expected" "actual" "delta"` + +Reports an error if `actual` is not within `delta` seconds of `expected`. + +All inputs are **epoch seconds** (integers), generated via `date +%s`. + +::: code-group +```bash [Example] +function test_success() { + local now + now="$(date +%s)" + local five_seconds_later=$(( now + 5 )) + + assert_date_within_delta "$now" "$five_seconds_later" "10" +} + +function test_failure() { + assert_date_within_delta "1700000000" "1700000020" "5" +} +``` +::: + ## assert_exit_code > `assert_exit_code "expected"` diff --git a/src/assert_dates.sh b/src/assert_dates.sh new file mode 100644 index 00000000..c8312e48 --- /dev/null +++ b/src/assert_dates.sh @@ -0,0 +1,149 @@ +#!/usr/bin/env bash + +function bashunit::date::to_epoch() { + local input="$1" + + # Already epoch seconds (all digits) + case "$input" in + *[!0-9]*) ;; # contains non-digits, continue to ISO parsing + *) + echo "$input" + return 0 + ;; + esac + + # ISO 8601 conversion (GNU vs BSD date) + local epoch + # Try GNU date first (-d flag) + epoch=$(date -d "$input" +%s 2>/dev/null) && { + echo "$epoch" + return 0 + } + # Try BSD date (-j -f flag) with datetime format + epoch=$(date -j -f "%Y-%m-%dT%H:%M:%S" "$input" +%s 2>/dev/null) && { + echo "$epoch" + return 0 + } + # Try BSD date with date-only format + epoch=$(date -j -f "%Y-%m-%d" "$input" +%s 2>/dev/null) && { + echo "$epoch" + return 0 + } + + # Unsupported format + echo "$input" + return 1 +} + +function assert_date_equals() { + bashunit::assert::should_skip && return 0 + + local expected + expected="$(bashunit::date::to_epoch "$1")" + local actual + actual="$(bashunit::date::to_epoch "$2")" + + if [[ "$actual" -ne "$expected" ]]; 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}" "${actual}" "to be equal to" "${expected}" + return + fi + + bashunit::state::add_assertions_passed +} + +function assert_date_before() { + bashunit::assert::should_skip && return 0 + + local expected + expected="$(bashunit::date::to_epoch "$1")" + local actual + actual="$(bashunit::date::to_epoch "$2")" + + if ! [[ "$actual" -lt "$expected" ]]; 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}" "${actual}" "to be before" "${expected}" + return + fi + + bashunit::state::add_assertions_passed +} + +function assert_date_after() { + bashunit::assert::should_skip && return 0 + + local expected + expected="$(bashunit::date::to_epoch "$1")" + local actual + actual="$(bashunit::date::to_epoch "$2")" + + if ! [[ "$actual" -gt "$expected" ]]; 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}" "${actual}" "to be after" "${expected}" + return + fi + + bashunit::state::add_assertions_passed +} + +function assert_date_within_range() { + bashunit::assert::should_skip && return 0 + + local from + from="$(bashunit::date::to_epoch "$1")" + local to + to="$(bashunit::date::to_epoch "$2")" + local actual + actual="$(bashunit::date::to_epoch "$3")" + + if [[ "$actual" -lt "$from" ]] || [[ "$actual" -gt "$to" ]]; 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}" "${actual}" "to be between" "${from} and ${to}" + return + fi + + bashunit::state::add_assertions_passed +} + +function assert_date_within_delta() { + bashunit::assert::should_skip && return 0 + + local expected + expected="$(bashunit::date::to_epoch "$1")" + local actual + actual="$(bashunit::date::to_epoch "$2")" + local delta="$3" + + local diff=$((actual - expected)) + if [[ "$diff" -lt 0 ]]; then + diff=$((-diff)) + fi + + if [[ "$diff" -gt "$delta" ]]; 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}" "${actual}" "to be within" "${delta} seconds of ${expected}" + return + fi + + bashunit::state::add_assertions_passed +} diff --git a/src/assertions.sh b/src/assertions.sh index 2b76b4ce..2b0c021c 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_dates.sh" source "$BASHUNIT_ROOT_DIR/src/assert_files.sh" source "$BASHUNIT_ROOT_DIR/src/assert_folders.sh" source "$BASHUNIT_ROOT_DIR/src/assert_snapshot.sh" 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 5144d03a..35be0b30 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 @@ -1,179 +1,500 @@ ## assert_true -------------- -::ignore:: +> `assert_true bool|function|command` + +Reports an error if the argument result in a truthy value: `true` or `0`. + +- assert_false is similar but different. + ## assert_false -------------- -::ignore:: +> `assert_false bool|function|command` + +Reports an error if the argument result in a falsy value: `false` or `1`. + +- assert_true is similar but different. + ## assert_same -------------- -::ignore:: +> `assert_same "expected" "actual"` + +Reports an error if the `expected` and `actual` are not the same - including special chars. + +- assert_not_same is the inverse of this assertion and takes the same arguments. +- assert_equals is similar but ignoring the special chars. + ## assert_equals -------------- -::ignore:: +> `assert_equals "expected" "actual"` + +Reports an error if the two variables `expected` and `actual` are not equal ignoring the special chars like ANSI Escape Sequences (colors) and other special chars like tabs and new lines. + +- assert_same is similar but including special chars. + ## assert_contains -------------- -::ignore:: +> `assert_contains "needle" "haystack"` + +Reports an error if `needle` is not a substring of `haystack`. + +- assert_not_contains is the inverse of this assertion and takes the same arguments. + ## assert_contains_ignore_case -------------- -::ignore:: +> `assert_contains_ignore_case "needle" "haystack"` + +Reports an error if `needle` is not a substring of `haystack`. +Differences in casing are ignored when needle is searched for in haystack. + ## assert_empty -------------- -::ignore:: +> `assert_empty "actual"` + +Reports an error if `actual` is not empty. + +- assert_not_empty is the inverse of this assertion and takes the same arguments. + ## assert_matches -------------- -::ignore:: +> `assert_matches "pattern" "value"` + +Reports an error if `value` does not match the regular expression `pattern`. + +- assert_not_matches is the inverse of this assertion and takes the same arguments. + ## assert_string_starts_with -------------- -::ignore:: +> `assert_string_starts_with "needle" "haystack"` + +Reports an error if `haystack` does not starts with `needle`. + +- assert_string_not_starts_with is the inverse of this assertion and takes the same arguments. + ## assert_string_ends_with -------------- -::ignore:: +> `assert_string_ends_with "needle" "haystack"` + +Reports an error if `haystack` does not ends with `needle`. + +- assert_string_not_ends_with is the inverse of this assertion and takes the same arguments. + ## assert_line_count -------------- -::ignore:: +> `assert_line_count "count" "haystack"` + +Reports an error if `haystack` does not contain `count` lines. + ## assert_less_than -------------- -::ignore:: +> `assert_less_than "expected" "actual"` + +Reports an error if `actual` is not less than `expected`. + +- assert_greater_than is the inverse of this assertion and takes the same arguments. + ## assert_less_or_equal_than -------------- -::ignore:: +> `assert_less_or_equal_than "expected" "actual"` + +Reports an error if `actual` is not less than or equal to `expected`. + +- assert_greater_than is the inverse of this assertion and takes the same arguments. + ## assert_greater_than -------------- -::ignore:: +> `assert_greater_than "expected" "actual"` + +Reports an error if `actual` is not greater than `expected`. + +- assert_less_than is the inverse of this assertion and takes the same arguments. + ## assert_greater_or_equal_than -------------- -::ignore:: +> `assert_greater_or_equal_than "expected" "actual"` + +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_date_equals +-------------- +> `assert_date_equals "expected" "actual"` + +Reports an error if the two epoch timestamps `expected` and `actual` are not equal. + +All inputs are **epoch seconds** (integers), generated via `date +%s`. + + +## assert_date_before +-------------- +> `assert_date_before "expected" "actual"` + +Reports an error if `actual` is not before `expected` (i.e. `actual` must be less than `expected`). + +All inputs are **epoch seconds** (integers), generated via `date +%s`. + + +## assert_date_after +-------------- +> `assert_date_after "expected" "actual"` + +Reports an error if `actual` is not after `expected` (i.e. `actual` must be greater than `expected`). + +All inputs are **epoch seconds** (integers), generated via `date +%s`. + + +## assert_date_within_range +-------------- +> `assert_date_within_range "from" "to" "actual"` + +Reports an error if `actual` does not fall between `from` and `to` (inclusive). + +All inputs are **epoch seconds** (integers), generated via `date +%s`. + + +## assert_date_within_delta +-------------- +> `assert_date_within_delta "expected" "actual" "delta"` + +Reports an error if `actual` is not within `delta` seconds of `expected`. + +All inputs are **epoch seconds** (integers), generated via `date +%s`. + ## assert_exit_code -------------- -::ignore:: +> `assert_exit_code "expected"` + +Reports an error if the exit code of the last executed command is not equal to `expected`. + +This assertion captures `$?` from the command executed **before** calling the assertion. +It does **not** execute a string command passed as a second parameter. + +::: tip +Use assert_exec if you want to pass a command as a string and check its exit code: +`assert_exec "your_command" --exit 0` +::: + +- assert_successful_code, assert_unsuccessful_code, assert_general_error and assert_command_not_found +are more semantic versions of this assertion, for which you don't need to specify an exit code. + + +## assert_exec +-------------- +> `assert_exec "command" --exit --stdout "text" --stderr "text"` + +Runs `command` capturing its exit status, standard output and standard error and +checks all provided expectations. When `--exit` is omitted the expected exit +status defaults to `0`. + ## assert_array_contains -------------- -::ignore:: +> `assert_array_contains "needle" "haystack"` + +Reports an error if `needle` is not an element of `haystack`. + +- assert_array_not_contains is the inverse of this assertion and takes the same arguments. + ## assert_successful_code -------------- -::ignore:: +> `assert_successful_code` + +Reports an error if the exit code of the last executed command is not successful (`0`). + +This assertion captures `$?` from the command executed **before** calling the assertion. +It does **not** execute a string command passed as a parameter. + +::: tip +Use assert_exec if you want to pass a command as a string and check its exit code: +`assert_exec "your_command"` (defaults to expecting exit code 0) +::: + +- assert_exit_code is the full version of this assertion where you can specify the expected exit code. + + +## assert_unsuccessful_code +-------------- +> `assert_unsuccessful_code` + +Reports an error if the exit code of the last executed command is not unsuccessful (non-zero). + +This assertion captures `$?` from the command executed **before** calling the assertion. +It does **not** execute a string command passed as a parameter. + +::: tip +Use assert_exec if you want to pass a command as a string and check its exit code: +`assert_exec "your_command" --exit 1` +::: + +- assert_exit_code is the full version of this assertion where you can specify the expected exit code. + ## assert_general_error -------------- -::ignore:: +> `assert_general_error` + +Reports an error if the exit code of the last executed command is not a general error (`1`). + +This assertion captures `$?` from the command executed **before** calling the assertion. +It does **not** execute a string command passed as a parameter. + +::: tip +Use assert_exec if you want to pass a command as a string and check its exit code: +`assert_exec "your_command" --exit 1` +::: + +- assert_exit_code is the full version of this assertion where you can specify the expected exit code. + ## assert_command_not_found -------------- -::ignore:: +> `assert_command_not_found` + +Reports an error if the last executed command did not return a "command not found" exit code (`127`). + +This assertion captures `$?` from the command executed **before** calling the assertion. +It does **not** execute a string command passed as a parameter. + +::: tip +Use assert_exec if you want to pass a command as a string and check its exit code: +`assert_exec "nonexistent_command" --exit 127` +::: + +- assert_exit_code is the full version of this assertion where you can specify the expected exit code. + ## assert_file_exists -------------- -::ignore:: +> `assert_file_exists "file"` + +Reports an error if `file` does not exists, or it is a directory. + +- assert_file_not_exists is the inverse of this assertion and takes the same arguments. + ## assert_file_contains -------------- -::ignore:: +> `assert_file_contains "file" "search"` + +Reports an error if `file` does not contains the search string. + +- assert_file_not_contains is the inverse of this assertion and takes the same arguments. + ## assert_is_file -------------- -::ignore:: +> `assert_is_file "file"` + +Reports an error if `file` is not a file. + ## assert_is_file_empty -------------- -::ignore:: +> `assert_is_file_empty "file"` + +Reports an error if `file` is not empty. + ## assert_directory_exists -------------- -::ignore:: +> `assert_directory_exists "directory"` + +Reports an error if `directory` does not exist. + +- assert_directory_not_exists is the inverse of this assertion and takes the same arguments. + ## assert_is_directory -------------- -::ignore:: +> `assert_is_directory "directory"` + +Reports an error if `directory` is not a directory. + ## assert_is_directory_empty -------------- -::ignore:: +> `assert_is_directory_empty "directory"` + +Reports an error if `directory` is not an empty directory. + +- assert_is_directory_not_empty is the inverse of this assertion and takes the same arguments. + ## assert_is_directory_readable -------------- -::ignore:: +> `assert_is_directory_readable "directory"` + +Reports an error if `directory` is not a readable directory. + +- assert_is_directory_not_readable is the inverse of this assertion and takes the same arguments. + ## assert_is_directory_writable -------------- -::ignore:: +> `assert_is_directory_writable "directory"` + +Reports an error if `directory` is not a writable directory. + +- assert_is_directory_not_writable is the inverse of this assertion and takes the same arguments. + ## assert_files_equals -------------- -::ignore:: +> `assert_files_equals "expected" "actual"` + +Reports an error if `expected` and `actual` are not equals. + +- assert_files_not_equals is the inverse of this assertion and takes the same arguments. + ## assert_not_same -------------- -::ignore:: +> `assert_not_same "expected" "actual"` + +Reports an error if the two variables `expected` and `actual` are the same value. + +- assert_same is the inverse of this assertion and takes the same arguments. + ## assert_not_contains -------------- -::ignore:: +> `assert_not_contains "needle" "haystack"` + +Reports an error if `needle` is a substring of `haystack`. + +- assert_contains is the inverse of this assertion and takes the same arguments. + ## assert_string_not_starts_with -------------- -::ignore:: +> `assert_string_not_starts_with "needle" "haystack"` + +Reports an error if `haystack` does starts with `needle`. + +- assert_string_starts_with is the inverse of this assertion and takes the same arguments. + ## assert_string_not_ends_with -------------- -::ignore:: +> `assert_string_not_ends_with "needle" "haystack"` + +Reports an error if `haystack` does ends with `needle`. + +- assert_string_ends_with is the inverse of this assertion and takes the same arguments. + ## assert_not_empty -------------- -::ignore:: +> `assert_not_empty "actual"` + +Reports an error if `actual` is empty. + +- assert_empty is the inverse of this assertion and takes the same arguments. + ## assert_not_matches -------------- -::ignore:: +> `assert_not_matches "pattern" "value"` + +Reports an error if `value` matches the regular expression `pattern`. + +- assert_matches is the inverse of this assertion and takes the same arguments. + ## assert_array_not_contains -------------- -::ignore:: +> `assert_array_not_contains "needle" "haystack"` + +Reports an error if `needle` is an element of `haystack`. + +- assert_array_contains is the inverse of this assertion and takes the same arguments. + ## assert_file_not_exists -------------- -::ignore:: +> `assert_file_not_exists "file"` + +Reports an error if `file` does exists. + +- assert_file_exists is the inverse of this assertion and takes the same arguments. + ## assert_file_not_contains -------------- -::ignore:: +> `assert_file_not_contains "file" "search"` + +Reports an error if `file` contains the search string. + +- assert_file_contains is the inverse of this assertion and takes the same arguments. + ## assert_directory_not_exists -------------- -::ignore:: +> `assert_directory_not_exists "directory"` + +Reports an error if `directory` exists. + +- assert_directory_exists is the inverse of this assertion and takes the same arguments. + ## assert_is_directory_not_empty -------------- -::ignore:: +> `assert_is_directory_not_empty "directory"` + +Reports an error if `directory` is empty. + +- assert_is_directory_empty is the inverse of this assertion and takes the same arguments. + ## assert_is_directory_not_readable -------------- -::ignore:: +> `assert_is_directory_not_readable "directory"` + +Reports an error if `directory` is readable. + +- assert_is_directory_readable is the inverse of this assertion and takes the same arguments. + ## assert_is_directory_not_writable -------------- -::ignore:: +> `assert_is_directory_not_writable "directory"` + +Reports an error if `directory` is writable. + +- assert_is_directory_writable is the inverse of this assertion and takes the same arguments. + ## assert_files_not_equals -------------- -::ignore:: +> `assert_files_not_equals "expected" "actual"` + +Reports an error if `expected` and `actual` are not equals. + +- assert_files_equals is the inverse of this assertion and takes the same arguments. + ## bashunit::fail -------------- -::ignore:: +> `bashunit::fail "failure message"` + +Unambiguously reports an error message. Useful for reporting specific message +when testing situations not covered by any `assert_*` functions. + + diff --git a/tests/acceptance/snapshots/bashunit_test_sh.test_bashunit_should_display_filtered_assert_docs.snapshot b/tests/acceptance/snapshots/bashunit_test_sh.test_bashunit_should_display_filtered_assert_docs.snapshot index 63c6ee31..a1b53737 100644 --- a/tests/acceptance/snapshots/bashunit_test_sh.test_bashunit_should_display_filtered_assert_docs.snapshot +++ b/tests/acceptance/snapshots/bashunit_test_sh.test_bashunit_should_display_filtered_assert_docs.snapshot @@ -7,6 +7,15 @@ Reports an error if the two variables `expected` and `actual` are not equal igno - assert_same is similar but including special chars. +## assert_date_equals +-------------- +> `assert_date_equals "expected" "actual"` + +Reports an error if the two epoch timestamps `expected` and `actual` are not equal. + +All inputs are **epoch seconds** (integers), generated via `date +%s`. + + ## assert_files_equals -------------- > `assert_files_equals "expected" "actual"` @@ -23,3 +32,5 @@ Reports an error if `expected` and `actual` are not equals. Reports an error if `expected` and `actual` are not equals. - assert_files_equals is the inverse of this assertion and takes the same arguments. + + diff --git a/tests/unit/assert_dates_test.sh b/tests/unit/assert_dates_test.sh new file mode 100644 index 00000000..7da62636 --- /dev/null +++ b/tests/unit/assert_dates_test.sh @@ -0,0 +1,131 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2327 +# shellcheck disable=SC2328 +# shellcheck disable=SC2329 + +function test_successful_assert_date_equals() { + assert_empty "$(assert_date_equals "1700000000" "1700000000")" +} + +function test_unsuccessful_assert_date_equals() { + assert_same\ + "$(bashunit::console_results::print_failed_test\ + "Unsuccessful assert date equals" "1600000000" "to be equal to" "1700000000")"\ + "$(assert_date_equals "1700000000" "1600000000")" +} + +function test_successful_assert_date_before() { + assert_empty "$(assert_date_before "1700000000" "1600000000")" +} + +function test_unsuccessful_assert_date_before() { + assert_same\ + "$(bashunit::console_results::print_failed_test\ + "Unsuccessful assert date before" "1800000000" "to be before" "1700000000")"\ + "$(assert_date_before "1700000000" "1800000000")" +} + +function test_unsuccessful_assert_date_before_when_equal() { + assert_same\ + "$(bashunit::console_results::print_failed_test\ + "Unsuccessful assert date before when equal" "1700000000" "to be before" "1700000000")"\ + "$(assert_date_before "1700000000" "1700000000")" +} + +function test_successful_assert_date_after() { + assert_empty "$(assert_date_after "1600000000" "1700000000")" +} + +function test_unsuccessful_assert_date_after() { + assert_same\ + "$(bashunit::console_results::print_failed_test\ + "Unsuccessful assert date after" "1500000000" "to be after" "1600000000")"\ + "$(assert_date_after "1600000000" "1500000000")" +} + +function test_unsuccessful_assert_date_after_when_equal() { + assert_same\ + "$(bashunit::console_results::print_failed_test\ + "Unsuccessful assert date after when equal" "1600000000" "to be after" "1600000000")"\ + "$(assert_date_after "1600000000" "1600000000")" +} + +function test_successful_assert_date_within_range() { + assert_empty "$(assert_date_within_range "1600000000" "1800000000" "1700000000")" +} + +function test_successful_assert_date_within_range_at_lower_bound() { + assert_empty "$(assert_date_within_range "1600000000" "1800000000" "1600000000")" +} + +function test_successful_assert_date_within_range_at_upper_bound() { + assert_empty "$(assert_date_within_range "1600000000" "1800000000" "1800000000")" +} + +function test_unsuccessful_assert_date_within_range_above() { + assert_same\ + "$(bashunit::console_results::print_failed_test\ + "Unsuccessful assert date within range above" "1900000000"\ + "to be between" "1600000000 and 1800000000")"\ + "$(assert_date_within_range "1600000000" "1800000000" "1900000000")" +} + +function test_unsuccessful_assert_date_within_range_below() { + assert_same\ + "$(bashunit::console_results::print_failed_test\ + "Unsuccessful assert date within range below" "1500000000"\ + "to be between" "1600000000 and 1800000000")"\ + "$(assert_date_within_range "1600000000" "1800000000" "1500000000")" +} + +function test_successful_assert_date_within_delta() { + assert_empty "$(assert_date_within_delta "1700000000" "1700000005" "10")" +} + +function test_successful_assert_date_within_delta_exact() { + assert_empty "$(assert_date_within_delta "1700000000" "1700000010" "10")" +} + +function test_successful_assert_date_within_delta_negative_direction() { + assert_empty "$(assert_date_within_delta "1700000010" "1700000000" "10")" +} + +function test_unsuccessful_assert_date_within_delta() { + assert_same\ + "$(bashunit::console_results::print_failed_test\ + "Unsuccessful assert date within delta" "1700000020"\ + "to be within" "5 seconds of 1700000000")"\ + "$(assert_date_within_delta "1700000000" "1700000020" "5")" +} + +# ISO 8601 auto-detection tests + +function test_successful_assert_date_equals_with_iso_dates() { + assert_empty "$(assert_date_equals "2023-06-15" "2023-06-15")" +} + +function test_successful_assert_date_before_with_iso_dates() { + assert_empty "$(assert_date_before "2024-01-01" "2023-01-01")" +} + +function test_successful_assert_date_after_with_iso_dates() { + assert_empty "$(assert_date_after "2023-01-01" "2024-01-01")" +} + +function test_successful_assert_date_within_range_with_iso_dates() { + assert_empty\ + "$(assert_date_within_range "2023-01-01" "2023-12-31" "2023-06-15")" +} + +function test_successful_assert_date_equals_with_mixed_formats() { + local epoch + epoch=$(date -d "2023-06-15" +%s 2>/dev/null) \ + || epoch=$(date -j -f "%Y-%m-%d" "2023-06-15" +%s 2>/dev/null) + + assert_empty "$(assert_date_equals "$epoch" "2023-06-15")" +} + +function test_successful_assert_date_within_delta_with_iso_datetime() { + assert_empty\ + "$(assert_date_within_delta "2023-11-14T12:00:00" "2023-11-14T12:00:05" "10")" +}