Skip to content

test(tools): pin find_peaks' four behaviours, and fix the docstring that described two of them wrongly - #816

Merged
ryanbr merged 2 commits into
mainfrom
test/find-peaks-coverage
Jul 26, 2026
Merged

test(tools): pin find_peaks' four behaviours, and fix the docstring that described two of them wrongly#816
ryanbr merged 2 commits into
mainfrom
test/find-peaks-coverage

Conversation

@ryanbr

@ryanbr ryanbr commented Jul 26, 2026

Copy link
Copy Markdown
Owner

Closes the coverage gap #815 recorded rather than fixed.

Mutation-testing #815 before merging showed its eight newly-collected spot-HRV tests catch a grossly broken peak detector but miss two of find_peaks' three behaviours:

mutation to find_peaks before after
drop the min_dist spacing filter survived killed
drop the min_prom prominence gate survived killed
invert the local-maximum comparison killed (2 tests) killed (4 tests)

test_find_peaks_counts_beats can't cover either by construction — its synthetic beats are already well separated and well above the noise floor, so both filters are no-ops on that input. Passing it says nothing about whether they exist.

These use minimal hand-built arrays, and each asserts both directions so it can't pass vacuously:

  • min_dist[0, 5, 0, 9, 0], maxima two samples apart. min_dist=3 keeps only the taller ([3]); min_dist=2 keeps both ([1, 3]).
  • min_prom[0, 2, 0, 9, 0]. min_prom=5.0 rejects the 2.0 maximum; min_prom=1.0 keeps it.

Mutants killed 3/3, up from 1/3. Suite 167 → 169, OK (1 skipped).

One practical note for anyone repeating the exercise: clear __pycache__ between mutations. A stale .pyc kept reporting failures from a mutant after the source had been restored — indistinguishable from a real regression in code that was actually clean.

Tests only. No app code, no tooling behaviour, no schema, no strings.

ryanbr added 2 commits July 25, 2026 20:08
Mutation-testing #815 before merge showed the eight newly-collected spot-HRV tests
catch a grossly broken peak detector but not two of find_peaks' three behaviours:
deleting the min_dist spacing filter left all eight passing, and so did deleting the
min_prom prominence gate. Only inverting the local-maximum comparison failed
anything. #815 recorded that as a known gap; this closes it.

test_find_peaks_counts_beats cannot cover either one by construction — its synthetic
beats are already well separated and well above the noise floor, so both filters are
no-ops on that input. These use minimal hand-built arrays instead, and each asserts
BOTH directions so it cannot pass vacuously:

  min_dist  [0,5,0,9,0], maxima two apart -> min_dist=3 keeps only the taller [3],
            min_dist=2 keeps both [1,3]
  min_prom  [0,2,0,9,0] -> min_prom=5 rejects the 2.0 maximum, min_prom=1 keeps it

Mutants now killed 3/3 where it was 1/3. Suite 167 -> 169, OK (1 skipped).

Note for anyone repeating this: clear __pycache__ between mutations. A stale .pyc
kept reporting failures from a mutant after the source had been restored, which
reads exactly like a real regression in code that is actually clean.
…them wrongly

Reviewing the previous commit turned up a docstring that contradicts the code it
documents, on two counts:

  docstring: "Local maxima >= neighbours and >= min_prom"
  code:      v[i] > v[i-1] and v[i] >= v[i+1] and v[i] > min_prom

min_prom is a STRICT >, so a maximum sitting exactly on the threshold is rejected,
not kept. And the neighbour test is ASYMMETRIC, not ">= neighbours": on a plateau
[0,5,5,0] it returns [1], the left edge only.

The code is right on both. The asymmetry is the standard way to take one index from
a plateau — symmetric >= would emit a duplicate per plateau sample and inflate the
beat count, symmetric > would drop plateaus entirely. So the docstring is corrected
rather than the behaviour, and both boundaries are now pinned by tests: a peak
exactly at min_prom is rejected, and a plateau of any length yields one index.

Mutation matrix, all with __pycache__ cleared between runs:

  drop min_dist spacing     killed
  drop min_prom gate        killed
  min_prom > becomes >=     killed   (new)
  neighbours symmetric >=   killed   (new)
  neighbours symmetric >    killed   (new)

5/5, up from 1/3 before this branch. Suite 169 -> 170, OK (1 skipped).

Worth noting the docstring had been wrong for as long as it existed and no test
could have caught it, because nothing exercised either boundary. That is the same
shape as the rest of today's tooling work: the gap was not a failing test, it was
the absence of one.
@ryanbr ryanbr changed the title test(tools): pin the two find_peaks behaviours nothing was guarding test(tools): pin find_peaks' four behaviours, and fix the docstring that described two of them wrongly Jul 26, 2026
@ryanbr
ryanbr merged commit b248c3d into main Jul 26, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant