Skip to content

perf(test): assert_duration_test spends 3s in real sleeps — mock measure_ms for failure paths #826

Description

@Chemaclass

Problem

tests/unit/assert_duration_test.sh takes ~3.3s for 7 tests — second-slowest unit file. Measured cause: three tests execute a real sleep 1:

  • test_unsuccessful_assert_duration_exceeds_thresholdassert_duration "sleep 1" 1000 (line 17)
  • test_unsuccessful_assert_duration_less_thanassert_duration_less_than "sleep 1" 100 (line 29)
  • test_successful_assert_duration_greater_thanassert_duration_greater_than "sleep 1" 500 (line 33)

That's 3s of wall time spent sleeping to test pure threshold-comparison logic.

Proposed fix

Two-part approach (don't just shrink all the sleeps — see the clock caveat below):

1. Failure-path tests (the two unsuccessful_* sleep tests): mock the measurement.
assert_duration / assert_duration_less_than / assert_duration_greater_than all delegate timing to bashunit::duration::measure_ms (src/assert_duration.sh:3) and then compare against the threshold. Mock it:

mock bashunit::duration::measure_ms echo 2000

Then assert_duration "sleep 1" 1000 can become assert_duration "fake_cmd" 1000 with a mocked elapsed of 2000ms — deterministic, zero sleep, and still exercises the real comparison + failure-message path. Note the expected failure message embeds the command string (print_failed_test ... "sleep 1"), so update the expected string to match the new command argument. Use bashunit's own mock helper (see tests/functional/doubles_test.sh for patterns).

Also applies to test_unsuccessful_assert_duration_greater_than (already fast, echo hello — can stay as-is or be unified with the mock style).

2. Success-path integration test (test_successful_assert_duration_greater_than): shrink the sleep, keep it real.
Keep one real-timing test so measure_ms integration stays covered: assert_duration_greater_than "sleep 0.2" 100 (2x margin).

⚠️ Clock resolution caveat: the clock has a last-resort fallback date-seconds with 1-second resolution (src/clock.sh:150, used only when EPOCHREALTIME, date +%s%N, perl, python, node, and powershell are all unavailable). Under that impl a sleep 0.2 measures 0ms and the greater_than test would fail. Guard it:

# in the test, before asserting:
bashunit::clock::is_expensive >/dev/null 2>&1 || true  # ensures impl chosen
if [ "$_BASHUNIT_CLOCK_NOW_IMPL" = "date-seconds" ]; then
  bashunit::skip "clock has 1s resolution" && return
fi

(Or keep sleep 1/500 for this single test if the guard is judged too intrusive — that alone still cuts the file from 3.3s to ~1.3s. Preferred: guard + sleep 0.2 → ~0.5s total.)

The two test_successful_*_within tests (sleep 0) and test_successful_assert_duration_less_than are already instant — leave them.

Files

  • tests/unit/assert_duration_test.sh — the only file to change
  • src/assert_duration.sh — read-only reference (measure_ms seam at line 3)
  • src/clock.sh:150date-seconds fallback (read-only reference)

Constraints

  • No changes to src/ — test-only
  • Bash 3.0+ compatible: no [[ ]], no Bash 4+ features (.claude/rules/bash-style.md)
  • sleep 0.2 (fractional) is an external-binary feature, fine on macOS/BSD/GNU/busybox — but only use it in the guarded integration test
  • TDD: one test at a time, suite green after each step
  • Safe under ./bashunit --parallel tests/

Acceptance criteria

  • time ./bashunit tests/unit/assert_duration_test.sh ≤ ~0.6s (baseline: ~3.3s)
  • Failure-path tests are deterministic (no real sleeps, mocked measure_ms), still assert the exact failure-message output
  • At least one success-path test still exercises real bashunit::duration::measure_ms timing end-to-end
  • No test flakes under seconds-resolution clock (date-seconds guarded or avoided)
  • ./bashunit tests/ and ./bashunit --parallel tests/ green
  • make sa, make lint pass; shfmt -w . produces no diff

Verification commands

time ./bashunit tests/unit/assert_duration_test.sh
./bashunit tests/ && ./bashunit --parallel tests/
make sa && make lint && shfmt -d .

Metadata

Metadata

Assignees

Labels

pure testingPure testing relatedrefactoringRefactoring or cleaning related

Type

No type

Projects

Status
Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions