Skip to content
Draft
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
102 changes: 102 additions & 0 deletions docs/assertions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"`

Expand Down
149 changes: 149 additions & 0 deletions src/assert_dates.sh
Original file line number Diff line number Diff line change
@@ -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
}
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_files.sh"
source "$BASHUNIT_ROOT_DIR/src/assert_folders.sh"
source "$BASHUNIT_ROOT_DIR/src/assert_snapshot.sh"
Expand Down
Loading
Loading