Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ jobs:
- name: "unit a-b"
test_path: "tests/unit/[a-b]*_test.sh"
- name: "unit c"
test_path: "tests/unit/c*_test.sh"
test_path: "tests/unit/ch*_test.sh tests/unit/cl*_test.sh tests/unit/console*_test.sh tests/unit/cu*_test.sh"
- name: "unit coverage"
test_path: "tests/unit/coverage_*_test.sh"
- name: "unit d-p"
test_path: "tests/unit/[d-p]*_test.sh"
- name: "unit r-z"
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
## Unreleased

### Added
- Add date comparison assertions: `assert_date_equals`, `assert_date_before`, `assert_date_after`, `assert_date_within_range`, `assert_date_within_delta`
- Auto-detects epoch seconds, ISO 8601, space-separated datetime, and timezone offsets
- Mixed formats supported in the same assertion call
- Add Claude Code configuration with custom skills, agents, and rules
- 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`)
Expand Down
110 changes: 110 additions & 0 deletions docs/assertions.md
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,116 @@ function test_failure() {
```
:::

## assert_date_equals
> `assert_date_equals "expected" "actual"`

Reports an error if the two date values `expected` and `actual` are not equal.

Inputs are automatically converted to epoch seconds. Supported formats:
- Epoch seconds (integers): `1700000000`
- ISO 8601 date: `2023-11-14`
- ISO 8601 datetime: `2023-11-14T12:00:00`
- ISO 8601 datetime with UTC Z: `2023-11-14T12:00:00Z`
- ISO 8601 datetime with timezone offset: `2023-11-14T12:00:00+0100`
- Space-separated datetime: `2023-11-14 12:00:00`

You can mix formats in the same assertion (e.g., one epoch, one ISO).

::: 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`).

Inputs are automatically converted to epoch seconds. See [assert_date_equals](#assert_date_equals) for supported formats.

::: 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`).

Inputs are automatically converted to epoch seconds. See [assert_date_equals](#assert_date_equals) for supported formats.

::: 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).

Inputs are automatically converted to epoch seconds. See [assert_date_equals](#assert_date_equals) for supported formats.

::: 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`.

Inputs are automatically converted to epoch seconds. See [assert_date_equals](#assert_date_equals) for supported formats.

::: 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"`

Expand Down
159 changes: 159 additions & 0 deletions src/assert_dates.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
#!/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

# Format 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 ISO 8601 datetime + timezone offset
epoch=$(date -j -f "%Y-%m-%dT%H:%M:%S%z" "$input" +%s 2>/dev/null) && {
echo "$epoch"
return 0
}
# Try BSD date with ISO 8601 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 space-separated datetime format
epoch=$(date -j -f "%Y-%m-%d %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
}
1 change: 1 addition & 0 deletions src/assertions.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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_duration.sh"
source "$BASHUNIT_ROOT_DIR/src/assert_files.sh"
source "$BASHUNIT_ROOT_DIR/src/assert_folders.sh"
Expand Down
Loading
Loading