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

Fields

No fields configured for issues without a type.

Projects

Status
Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions