Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
58181b3
fix(assert): join negative prefix haystacks
Chemaclass Oct 29, 2026
783e2db
fix(assert): validate BSD dates and colon offsets
Chemaclass Oct 29, 2026
676bac0
test(dates): update offset documentation snapshots
Chemaclass Oct 29, 2026
c645fd5
chore: merge preceding issue fixes
Chemaclass Oct 29, 2026
1a7fe47
chore: merge preceding issue fixes
Chemaclass Oct 29, 2026
f13c03e
chore: merge provider stop correction
Chemaclass Oct 29, 2026
5c7bf44
chore: merge provider stop correction
Chemaclass Oct 29, 2026
65eb450
chore: merge static-count guard correction
Chemaclass Oct 29, 2026
dfa7e97
chore: merge static-count guard correction
Chemaclass Oct 29, 2026
4fa1f78
chore: merge TAP test portability fix
Chemaclass Oct 29, 2026
30ac6bd
chore: merge TAP test portability fix
Chemaclass Oct 29, 2026
42174b8
chore: merge earlier static guard ancestry
Chemaclass Oct 29, 2026
b99e95e
chore: merge earlier static guard ancestry
Chemaclass Oct 29, 2026
55f6316
chore: merge regex pipe correction
Chemaclass Oct 29, 2026
bbc1d25
chore: merge regex pipe correction
Chemaclass Oct 29, 2026
8405ffc
chore: merge process-guide correction
Chemaclass Oct 29, 2026
fd22cad
chore: merge process-guide correction
Chemaclass Oct 29, 2026
c57eae8
docs: group unreleased changelog notes
Chemaclass Oct 30, 2026
6feacb3
chore: merge validated CI fixture fixes
Chemaclass Oct 30, 2026
4daedab
chore: merge validated CI fixture fixes
Chemaclass Oct 30, 2026
4c75a43
chore: merge validated CI fixture fixes
Chemaclass Oct 30, 2026
cfd40da
chore: merge terminal fixture readiness fix
Chemaclass Oct 30, 2026
d88b3bf
chore: merge terminal fixture readiness fix
Chemaclass Oct 30, 2026
97b4b55
chore: merge terminal fixture readiness fix
Chemaclass Oct 30, 2026
74c5640
chore: merge windows provider parsing fix
Chemaclass Oct 30, 2026
481bafb
chore: merge windows provider parsing fix
Chemaclass Oct 30, 2026
5288786
chore: merge contributor fix and terminal diagnostics
Chemaclass Oct 30, 2026
6f3db43
chore: drop superseded prefix assertion changes
Chemaclass Oct 30, 2026
a9ccce0
chore: merge contributor fix and terminal diagnostics
Chemaclass Oct 30, 2026
4f31cf1
chore: merge terminal phase budget correction
Chemaclass Oct 30, 2026
7fc53e9
chore: merge terminal phase budget correction
Chemaclass Oct 30, 2026
282eae8
Merge branch 'fix/1404-date-bsd-fail-closed' into docs/group-unreleas…
Chemaclass Oct 30, 2026
ec8413b
Merge branch 'fix/1402-snapshot-dash-value' into fix/1404-date-bsd-fa…
Chemaclass Oct 30, 2026
39dc722
docs: bound validation workers
Chemaclass Oct 30, 2026
06e3a66
Merge branch 'fix/1404-date-bsd-fail-closed' into docs/group-unreleas…
Chemaclass Oct 30, 2026
9b460b5
Merge branch 'fix/1402-snapshot-dash-value' into fix/1404-date-bsd-fa…
Chemaclass Oct 30, 2026
5ea6a1a
Merge pull request #1457 from TypedDevs/docs/group-unreleased-notes
Chemaclass Oct 30, 2026
1a42579
chore: merge docs and CI repairs into date fixes
Chemaclass Oct 30, 2026
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: 2 additions & 2 deletions .agnostic-ai/AGNOSTIC_AI.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ Preserve Bash 3.0 parsing and public APIs; follow sibling tests/code. Update doc
Main selects checks by changed behavior/risk and reuses unchanged passes. Small fixes: affected tests, ShellCheck, `make lint`; strict mode for options, errors, hooks, or workers. Cross-cutting runtime changes/releases need:

```bash
make test
make test/strict
BASHUNIT_PARALLEL_JOBS=4 make test
BASHUNIT_PARALLEL_JOBS=4 make test/strict
make sa
make lint
```
Expand Down
54 changes: 29 additions & 25 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,37 +9,41 @@

### Fixed

- Snapshots record and update literal `-n`, `-e`, `-E` and combined echo flag values correctly (#1402)
- Generated workflows run test directories containing whitespace as a single path without requiring a committed `.env` (#1425)
- Annotations, tags, header counts and duplicate checks support functions declared without parentheses (#1422)
- Echo data providers keep `#` and parentheses, and report malformed rows without dropping later rows. Quoted values keep carriage returns on Windows (#1419)
#### Documentation

- Homepage logo follows reduced-motion preferences and stops tracking the pointer after navigation
- Primary buttons use dark text in both themes, including hover and active states
- Quickstart commands match install.sh, npm project and npm global installs
- A green file or filtered subset preserves rerun-cache entries for tests it did not execute (#1418)
- SIGTERM and SIGHUP run teardown hooks, stop parallel workers, and remove the run's scratch files before exiting with status 1 (#1417)
- Sequential stop-on-failure runs write requested reports and the rerun cache for the tests that ran (#1415)
- Include, inline, and exclusion filters preserve `test_` inside names such as `latest_version` (#1414)
- `.bashunitrc` suite options stay out of the environment, while global settings before and after suite sections still apply (#1410)
- `install.sh` rejects prefixed or incomplete version numbers instead of treating them as directories (#1409)
- `install.sh` resolves relative destinations from the caller's directory and reports the resolved installation path (#1408)
- `bashunit::unmock` restores a shell function replaced by a spy, mock, or mock sequence (#1401)
- Spies keep a multiline argument in one recorded call, including call comparisons and failure logs (#1399)
- Parallel results and hook output stay separate for test paths that differ by slashes and underscores (#1416)
- Coverage report paths are checked before tests run, and LCOV or HTML write failures make the run fail instead of reporting success (#1412)
- `upgrade` keeps the installed binary when a download fails or returns an empty file (#1407)
- `install.sh` keeps the installed binary when a download fails, is empty, or fails checksum verification (#1406)
- `--debug` leaves an existing test file or directory as the test path instead of taking it as the trace file (#1405)
- Call argument assertions fail when the spy was never called or the requested call index does not exist (#1400)
- `assert_matches` and `assert_not_matches` accept patterns starting with `-` and fail when grep rejects a pattern (#1398)

#### Assertions and doubles

- Date assertions reject trailing text on macOS and parse colon-separated timezone offsets consistently across platforms (#1404)
- `assert_string_not_starts_with` joins every haystack argument and uses the test name as its failure label, matching its siblings (#1403). `assert_matches` and `assert_not_matches` accept patterns starting with `-` and fail when grep rejects a pattern (#1398)
- `assert_files_equals` and `assert_files_not_equals` fail and name the missing path when either file does not exist (#1397)
- TAP reports keep test titles containing line breaks on a single result line (#1413)
- Snapshots record and update literal `-n`, `-e`, `-E` and combined echo flag values correctly (#1402)
- Spies keep multiline arguments in one recorded call, including comparisons and failure logs (#1399). Call argument assertions fail when the spy was never called or the requested call index does not exist (#1400)
- `bashunit::unmock` restores shell functions replaced by spies, mocks or mock sequences (#1401)

#### Setup and selection

- `install.sh` and `upgrade` keep the installed binary when a download fails or is empty; installation also keeps it when checksum verification fails (#1406, #1407)
- `install.sh` resolves relative destinations from the caller's directory, reports the resolved path (#1408), and rejects prefixed or incomplete version numbers instead of treating them as directories (#1409)
- `init` quotes the bootstrap path and custom tests directory in `.env`, so paths containing spaces work on later runs (#1411). Generated workflows pass a tests directory containing whitespace as one path without requiring a committed `.env` (#1425)
- `.bashunitrc` suite options stay out of the environment; global settings before and after suite sections still apply (#1410)
- Include, inline and exclusion filters preserve `test_` inside names such as `latest_version` (#1414). `--debug` keeps an existing test file or directory as the test path instead of taking it as the trace file (#1405)

#### Test discovery and execution

- Annotations, tags, header counts and duplicate checks support functions declared without parentheses (#1422). Duplicate definitions with an opening brace on the next line are reported instead of replacing the first definition (#1421)
- Echo data providers keep `#` and parentheses, and report malformed rows without dropping later rows. Quoted values keep carriage returns on Windows (#1419)
- SIGTERM and SIGHUP run teardown hooks, stop parallel workers and remove scratch files before exiting with status 1 (#1417)
- Parallel results and hook output stay separate for paths that differ by slashes and underscores (#1416). Relative hidden paths are counted and keep separate results from matching visible paths (#1393)

#### Reports and reruns

- `init` writes the bootstrap path and a custom tests directory to `.env` quoted, so a directory with a space runs afterwards instead of breaking every next run (#1411)
- Sequential stop-on-failure runs write requested reports and the rerun cache for tests that ran (#1415). A green file or filtered subset preserves cache entries for tests it did not execute (#1418)
- Coverage report paths are checked before tests run; LCOV or HTML write failures make the run fail (#1412). TAP reports keep test titles containing line breaks on one result line (#1413)
- Failure source context stays inside a one-line test instead of listing the next function's assertions (#1420)
- Parallel runs count tests under relative hidden paths and keep their results separate from matching visible paths (#1393)
- Duplicate test functions are detected when the opening brace is on the next line instead of silently replacing the first definition (#1421)
- `assert_string_not_starts_with` treats variadic haystack arguments as one newline-joined value and keeps failures labeled with the test's name (#1403)

### Changed

Expand Down
2 changes: 1 addition & 1 deletion docs/assertions.md
Original file line number Diff line number Diff line change
Expand Up @@ -527,7 +527,7 @@ Inputs are automatically converted to epoch seconds. Supported formats:
- 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`
- ISO 8601 datetime with timezone offset: `2023-11-14T12:00:00+0100` or `2023-11-14T12:00:00+01:00`
- Space-separated datetime: `2023-11-14 12:00:00`

You can mix formats in the same assertion (e.g., one epoch, one ISO).
Expand Down
53 changes: 27 additions & 26 deletions src/assert/dates.sh
Original file line number Diff line number Diff line change
@@ -1,15 +1,28 @@
#!/usr/bin/env bash

function bashunit::date::_bsd_to_epoch() {
local input="$1" format
local date_pattern='[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]'
local time_pattern='[0-9][0-9]:[0-9][0-9]:[0-9][0-9]'
# shellcheck disable=SC2254 # Date-shape glob patterns must expand unquoted.
case "$input" in
$date_pattern"T"${time_pattern}[+-][0-9][0-9][0-9][0-9]) format="%Y-%m-%dT%H:%M:%S%z" ;;
$date_pattern"T"$time_pattern) format="%Y-%m-%dT%H:%M:%S" ;;
$date_pattern" "$time_pattern) format="%Y-%m-%d %H:%M:%S" ;;
$date_pattern)
input="$input 00:00:00"
format="%Y-%m-%d %H:%M:%S"
;;
*) return 1 ;;
esac
# BSD date accepts an unparsed suffix, so validate the complete shape first.
date -j -f "$format" "$input" +%s 2>/dev/null
}

function bashunit::date::to_epoch() {
local input="$1"

# An empty string is never a date, and it has to be rejected here rather than
# left to the cascade below, because the two date(1) implementations disagree
# about it: GNU reads `date -d ""` as "now" and exits 0, while on BSD/macOS
# -d is not even a valid option. Falling through therefore resolved two empty
# inputs to the same epoch and compared them equal on Linux while correctly
# failing on macOS. It also matched the all-digits fast path below vacuously
# (an empty string contains no non-digit characters).
# GNU date treats an empty input as now; date assertions must reject it.
if [ -z "$input" ]; then
echo "$input"
return 1
Expand All @@ -24,6 +37,10 @@ function bashunit::date::to_epoch() {
;;
esac

case "$input" in
*[+-][0-9][0-9]:[0-9][0-9]) input="${input%:??}${input##*:}" ;;
esac

# Handle Z (UTC) suffix explicitly: BusyBox needs TZ=UTC, BSD needs +0000
case "$input" in
*Z)
Expand All @@ -34,7 +51,7 @@ function bashunit::date::to_epoch() {
epoch=$(TZ=UTC date -d "$utc_input" +%s 2>/dev/null) && { echo "$epoch"; return 0; }
epoch=$(TZ=UTC date -d "$utc_norm" +%s 2>/dev/null) && { echo "$epoch"; return 0; }
# BSD: use +0000 offset which %z understands
epoch=$(date -j -f "%Y-%m-%dT%H:%M:%S%z" "${utc_input}+0000" +%s 2>/dev/null) && { echo "$epoch"; return 0; }
epoch=$(bashunit::date::_bsd_to_epoch "${utc_input}+0000") && { echo "$epoch"; return 0; }
echo "$input"
return 1
;;
Expand Down Expand Up @@ -82,29 +99,13 @@ function bashunit::date::to_epoch() {
return 0
}
fi
# 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 (append midnight for deterministic results)
epoch=$(date -j -f "%Y-%m-%d %H:%M:%S" "$input 00:00:00" +%s 2>/dev/null) && {
epoch=$(bashunit::date::_bsd_to_epoch "$input") && {
echo "$epoch"
return 0
}

# Unsupported format
echo "$input"
echo "$1"
return 1
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ Inputs are automatically converted to epoch seconds. Supported formats:
- 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`
- ISO 8601 datetime with timezone offset: `2023-11-14T12:00:00+0100` or `2023-11-14T12:00:00+01:00`
- Space-separated datetime: `2023-11-14 12:00:00`

You can mix formats in the same assertion (e.g., one epoch, one ISO).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Inputs are automatically converted to epoch seconds. Supported formats:
- 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`
- ISO 8601 datetime with timezone offset: `2023-11-14T12:00:00+0100` or `2023-11-14T12:00:00+01:00`
- Space-separated datetime: `2023-11-14 12:00:00`

You can mix formats in the same assertion (e.g., one epoch, one ISO).
Expand Down
38 changes: 38 additions & 0 deletions tests/unit/assert/dates_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,44 @@ function test_unsuccessful_assert_date_equals() {
"$(assert_date_equals "1700000000" "1600000000")"
}

function test_unsuccessful_assert_date_equals_with_trailing_text() {
assert_same\
"$(bashunit::console_results::print_failed_test\
"Unsuccessful assert date equals with trailing text"\
"2024-01-01T10:00:00 nonsense" "to be" "a valid date")"\
"$(assert_date_equals "2024-01-01T10:00:00 nonsense" "2024-01-01T10:00:00")"
}

function test_successful_assert_date_equals_with_colon_offset() {
assert_empty "$(assert_date_equals "2024-01-01T10:00:00+0200" "2024-01-01T10:00:00+02:00")"
assert_empty "$(assert_date_equals "1704096000" "2024-01-01T10:00:00+02:00")"
}

function test_successful_assert_date_equals_with_negative_colon_offset() {
assert_empty "$(assert_date_equals "2024-01-01T10:00:00-0430" "2024-01-01T10:00:00-04:30")"
assert_empty "$(assert_date_equals "1704119400" "2024-01-01T10:00:00-04:30")"
}

function test_date_parser_rejects_trailing_text_in_each_supported_shape() {
local input actual code
for input in \
'2024-01-01 nonsense' \
'2024-01-01 10:00:00 nonsense' \
'2024-01-01T10:00:00+0200 nonsense' \
'2024-01-01T10:00:00+02:00 nonsense' \
'2024-01-01T10:00:00 nonsenseZ'; do
code=0
actual=$(bashunit::date::to_epoch "$input") || code=$?
assert_same 1 "$code"
assert_same "$input" "$actual"
done
}

function test_successful_assert_date_equals_preserves_explicit_timezone_epochs() {
assert_empty "$(assert_date_equals "1704096000" "2024-01-01T10:00:00+0200")"
assert_empty "$(assert_date_equals "1704103200" "2024-01-01T10:00:00Z")"
}

function test_successful_assert_date_before() {
assert_empty "$(assert_date_before "1700000000" "1600000000")"
}
Expand Down
Loading