Skip to content

Commit aec3eb3

Browse files
authored
Merge pull request #585 from TypedDevs/feat/date-comparison-assserts
feat(assert): date comparison assertions with format auto-detection
2 parents 91ebb1d + 6c8ebe1 commit aec3eb3

9 files changed

Lines changed: 904 additions & 50 deletions

.github/workflows/tests.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,9 @@ jobs:
4949
- name: "unit a-b"
5050
test_path: "tests/unit/[a-b]*_test.sh"
5151
- name: "unit c"
52-
test_path: "tests/unit/c*_test.sh"
52+
test_path: "tests/unit/ch*_test.sh tests/unit/cl*_test.sh tests/unit/console*_test.sh tests/unit/cu*_test.sh"
53+
- name: "unit coverage"
54+
test_path: "tests/unit/coverage_*_test.sh"
5355
- name: "unit d-p"
5456
test_path: "tests/unit/[d-p]*_test.sh"
5557
- name: "unit r-z"

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
## Unreleased
44

55
### Added
6+
- Add date comparison assertions: `assert_date_equals`, `assert_date_before`, `assert_date_after`, `assert_date_within_range`, `assert_date_within_delta`
7+
- Auto-detects epoch seconds, ISO 8601, space-separated datetime, and timezone offsets
8+
- Mixed formats supported in the same assertion call
9+
- Add Claude Code configuration with custom skills, agents, and rules
610
- Add `assert_have_been_called_nth_with` for verifying arguments on the Nth invocation of a spy
711
- Add `assert_string_matches_format` and `assert_string_not_matches_format` with format placeholders (`%d`, `%s`, `%f`, `%i`, `%x`, `%e`, `%%`)
812
- Add JSON assertions: `assert_json_key_exists`, `assert_json_contains`, `assert_json_equals` (requires `jq`)

docs/assertions.md

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,116 @@ function test_failure() {
356356
```
357357
:::
358358

359+
## assert_date_equals
360+
> `assert_date_equals "expected" "actual"`
361+
362+
Reports an error if the two date values `expected` and `actual` are not equal.
363+
364+
Inputs are automatically converted to epoch seconds. Supported formats:
365+
- Epoch seconds (integers): `1700000000`
366+
- ISO 8601 date: `2023-11-14`
367+
- ISO 8601 datetime: `2023-11-14T12:00:00`
368+
- ISO 8601 datetime with UTC Z: `2023-11-14T12:00:00Z`
369+
- ISO 8601 datetime with timezone offset: `2023-11-14T12:00:00+0100`
370+
- Space-separated datetime: `2023-11-14 12:00:00`
371+
372+
You can mix formats in the same assertion (e.g., one epoch, one ISO).
373+
374+
::: code-group
375+
```bash [Example]
376+
function test_success() {
377+
local now
378+
now="$(date +%s)"
379+
380+
assert_date_equals "$now" "$now"
381+
}
382+
383+
function test_failure() {
384+
assert_date_equals "1700000000" "1600000000"
385+
}
386+
```
387+
:::
388+
389+
## assert_date_before
390+
> `assert_date_before "expected" "actual"`
391+
392+
Reports an error if `actual` is not before `expected` (i.e. `actual` must be less than `expected`).
393+
394+
Inputs are automatically converted to epoch seconds. See [assert_date_equals](#assert_date_equals) for supported formats.
395+
396+
::: code-group
397+
```bash [Example]
398+
function test_success() {
399+
assert_date_before "1700000000" "1600000000"
400+
}
401+
402+
function test_failure() {
403+
assert_date_before "1700000000" "1800000000"
404+
}
405+
```
406+
:::
407+
408+
## assert_date_after
409+
> `assert_date_after "expected" "actual"`
410+
411+
Reports an error if `actual` is not after `expected` (i.e. `actual` must be greater than `expected`).
412+
413+
Inputs are automatically converted to epoch seconds. See [assert_date_equals](#assert_date_equals) for supported formats.
414+
415+
::: code-group
416+
```bash [Example]
417+
function test_success() {
418+
assert_date_after "1600000000" "1700000000"
419+
}
420+
421+
function test_failure() {
422+
assert_date_after "1600000000" "1500000000"
423+
}
424+
```
425+
:::
426+
427+
## assert_date_within_range
428+
> `assert_date_within_range "from" "to" "actual"`
429+
430+
Reports an error if `actual` does not fall between `from` and `to` (inclusive).
431+
432+
Inputs are automatically converted to epoch seconds. See [assert_date_equals](#assert_date_equals) for supported formats.
433+
434+
::: code-group
435+
```bash [Example]
436+
function test_success() {
437+
assert_date_within_range "1600000000" "1800000000" "1700000000"
438+
}
439+
440+
function test_failure() {
441+
assert_date_within_range "1600000000" "1800000000" "1900000000"
442+
}
443+
```
444+
:::
445+
446+
## assert_date_within_delta
447+
> `assert_date_within_delta "expected" "actual" "delta"`
448+
449+
Reports an error if `actual` is not within `delta` seconds of `expected`.
450+
451+
Inputs are automatically converted to epoch seconds. See [assert_date_equals](#assert_date_equals) for supported formats.
452+
453+
::: code-group
454+
```bash [Example]
455+
function test_success() {
456+
local now
457+
now="$(date +%s)"
458+
local five_seconds_later=$(( now + 5 ))
459+
460+
assert_date_within_delta "$now" "$five_seconds_later" "10"
461+
}
462+
463+
function test_failure() {
464+
assert_date_within_delta "1700000000" "1700000020" "5"
465+
}
466+
```
467+
:::
468+
359469
## assert_exit_code
360470
> `assert_exit_code "expected"`
361471

src/assert_dates.sh

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
#!/usr/bin/env bash
2+
3+
function bashunit::date::to_epoch() {
4+
local input="$1"
5+
6+
# Already epoch seconds (all digits)
7+
case "$input" in
8+
*[!0-9]*) ;; # contains non-digits, continue to ISO parsing
9+
*)
10+
echo "$input"
11+
return 0
12+
;;
13+
esac
14+
15+
# Format conversion (GNU vs BSD date)
16+
local epoch
17+
# Try GNU date first (-d flag)
18+
epoch=$(date -d "$input" +%s 2>/dev/null) && {
19+
echo "$epoch"
20+
return 0
21+
}
22+
# Try BSD date (-j -f flag) with ISO 8601 datetime + timezone offset
23+
epoch=$(date -j -f "%Y-%m-%dT%H:%M:%S%z" "$input" +%s 2>/dev/null) && {
24+
echo "$epoch"
25+
return 0
26+
}
27+
# Try BSD date with ISO 8601 datetime format
28+
epoch=$(date -j -f "%Y-%m-%dT%H:%M:%S" "$input" +%s 2>/dev/null) && {
29+
echo "$epoch"
30+
return 0
31+
}
32+
# Try BSD date with space-separated datetime format
33+
epoch=$(date -j -f "%Y-%m-%d %H:%M:%S" "$input" +%s 2>/dev/null) && {
34+
echo "$epoch"
35+
return 0
36+
}
37+
# Try BSD date with date-only format
38+
epoch=$(date -j -f "%Y-%m-%d" "$input" +%s 2>/dev/null) && {
39+
echo "$epoch"
40+
return 0
41+
}
42+
43+
# Unsupported format
44+
echo "$input"
45+
return 1
46+
}
47+
48+
function assert_date_equals() {
49+
bashunit::assert::should_skip && return 0
50+
51+
local expected
52+
expected="$(bashunit::date::to_epoch "$1")"
53+
local actual
54+
actual="$(bashunit::date::to_epoch "$2")"
55+
56+
if [[ "$actual" -ne "$expected" ]]; then
57+
local test_fn
58+
test_fn="$(bashunit::helper::find_test_function_name)"
59+
local label
60+
label="$(bashunit::helper::normalize_test_function_name "$test_fn")"
61+
bashunit::assert::mark_failed
62+
bashunit::console_results::print_failed_test "${label}" "${actual}" "to be equal to" "${expected}"
63+
return
64+
fi
65+
66+
bashunit::state::add_assertions_passed
67+
}
68+
69+
function assert_date_before() {
70+
bashunit::assert::should_skip && return 0
71+
72+
local expected
73+
expected="$(bashunit::date::to_epoch "$1")"
74+
local actual
75+
actual="$(bashunit::date::to_epoch "$2")"
76+
77+
if ! [[ "$actual" -lt "$expected" ]]; then
78+
local test_fn
79+
test_fn="$(bashunit::helper::find_test_function_name)"
80+
local label
81+
label="$(bashunit::helper::normalize_test_function_name "$test_fn")"
82+
bashunit::assert::mark_failed
83+
bashunit::console_results::print_failed_test "${label}" "${actual}" "to be before" "${expected}"
84+
return
85+
fi
86+
87+
bashunit::state::add_assertions_passed
88+
}
89+
90+
function assert_date_after() {
91+
bashunit::assert::should_skip && return 0
92+
93+
local expected
94+
expected="$(bashunit::date::to_epoch "$1")"
95+
local actual
96+
actual="$(bashunit::date::to_epoch "$2")"
97+
98+
if ! [[ "$actual" -gt "$expected" ]]; then
99+
local test_fn
100+
test_fn="$(bashunit::helper::find_test_function_name)"
101+
local label
102+
label="$(bashunit::helper::normalize_test_function_name "$test_fn")"
103+
bashunit::assert::mark_failed
104+
bashunit::console_results::print_failed_test "${label}" "${actual}" "to be after" "${expected}"
105+
return
106+
fi
107+
108+
bashunit::state::add_assertions_passed
109+
}
110+
111+
function assert_date_within_range() {
112+
bashunit::assert::should_skip && return 0
113+
114+
local from
115+
from="$(bashunit::date::to_epoch "$1")"
116+
local to
117+
to="$(bashunit::date::to_epoch "$2")"
118+
local actual
119+
actual="$(bashunit::date::to_epoch "$3")"
120+
121+
if [[ "$actual" -lt "$from" ]] || [[ "$actual" -gt "$to" ]]; then
122+
local test_fn
123+
test_fn="$(bashunit::helper::find_test_function_name)"
124+
local label
125+
label="$(bashunit::helper::normalize_test_function_name "$test_fn")"
126+
bashunit::assert::mark_failed
127+
bashunit::console_results::print_failed_test "${label}" "${actual}" "to be between" "${from} and ${to}"
128+
return
129+
fi
130+
131+
bashunit::state::add_assertions_passed
132+
}
133+
134+
function assert_date_within_delta() {
135+
bashunit::assert::should_skip && return 0
136+
137+
local expected
138+
expected="$(bashunit::date::to_epoch "$1")"
139+
local actual
140+
actual="$(bashunit::date::to_epoch "$2")"
141+
local delta="$3"
142+
143+
local diff=$((actual - expected))
144+
if [[ "$diff" -lt 0 ]]; then
145+
diff=$((-diff))
146+
fi
147+
148+
if [[ "$diff" -gt "$delta" ]]; then
149+
local test_fn
150+
test_fn="$(bashunit::helper::find_test_function_name)"
151+
local label
152+
label="$(bashunit::helper::normalize_test_function_name "$test_fn")"
153+
bashunit::assert::mark_failed
154+
bashunit::console_results::print_failed_test "${label}" "${actual}" "to be within" "${delta} seconds of ${expected}"
155+
return
156+
fi
157+
158+
bashunit::state::add_assertions_passed
159+
}

src/assertions.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
source "$BASHUNIT_ROOT_DIR/src/assert.sh"
44
source "$BASHUNIT_ROOT_DIR/src/assert_arrays.sh"
5+
source "$BASHUNIT_ROOT_DIR/src/assert_dates.sh"
56
source "$BASHUNIT_ROOT_DIR/src/assert_duration.sh"
67
source "$BASHUNIT_ROOT_DIR/src/assert_files.sh"
78
source "$BASHUNIT_ROOT_DIR/src/assert_folders.sh"

0 commit comments

Comments
 (0)