Skip to content

Make the self-test capable of failing, and cover the FFT lengths it never reached - #216

Open
MarkRose wants to merge 13 commits into
primesearch:mainfrom
MarkRose:fix-selftest-blindness
Open

MarkRose wants to merge 13 commits into
primesearch:mainfrom
MarkRose:fix-selftest-blindness

Conversation

@MarkRose

@MarkRose MarkRose commented Jul 30, 2026 •

Copy link
Copy Markdown
Member

The self-test has a good correctness check and throws its verdict away. This makes it capable of failing, and extends it to the FFT lengths it never reached.

The core defect

Mlucas.c:4853-4855:

DONE:
	fprintf(stderr, "\n  Done ...\n\n");
	return(0);

-s and -fft/-radset always exit 0. Measured on a stock nosimd build of 418f365:

$ ./Mlucas -s tt ; echo "exit=$?"
WARNING: 1 of 3 radix-sets at FFT length 4 K passed - skipping it. PLEASE CHECK YOUR BUILD OPTIONS.
WARNING: 1 of 3 radix-sets at FFT length 5 K passed - skipping it. PLEASE CHECK YOUR BUILD OPTIONS.
...
exit=0

And ci.yml's four self-test steps gate on that status — three of them additionally pipe into tee, so even a corrected status would be swallowed by the pipeline. No self-test regression could ever have turned CI red.

Four more, underneath

  1. The new-data consensus anchor is if(!new_res.sh0). A first-radset Res64 == 0 leaves the anchor falsy, so every radix set is scored as passing whatever it computed. On pristine main, -m 18776473 -fft 992 -iters 100 prints Res64: 0000000000000000. AvgMaxErr = 0.000000874 and writes that zero into mlucas.cfg as a certified "2 of 2 consensus". Exposure: 42 of 136 MvecPRP rows, the 10000-iter column of 17 MersVec rows, and every off-table -fft.
  2. resFlag = 0 on a match erases an earlier Res64 mismatch.
  3. No zero-residue rejection when there is no reference — the zero is adopted as the reference.
  4. The "length not in the table" test is dead code: i != MvecPtr[j].fftLength is evaluated at j == numTest, against the row just written a few dozen lines earlier, so it is never true. -fft 992 -iters 100 refused to run at all.

Note that fracmax/AvgMaxErr cannot substitute for any of this: it measures distance from the nearest integer, and zero is an integer, so an all-zero residue reports perfect numerical health.

Coverage, measured rather than assumed

The "56 of 603" figure in circulation compares two different populations — 56 is the nosimd Teensy count, 603 the SIMD total. Enumerated properly via get_fft_radices() in a harness linked against the real objects: 153 legal lengths / 603 (fftlen, radix_set) pairs under SIMD, 708 under nosimd. -s a covers 454/603 (75%); all tiers together 564/603 (93.5%). Per tier on avx2: tt 26, t 93, s 127, m 145, l 63, h 67, e 43 — reproduced exactly by the new summary line.

The gap that matters is not the 6.5%. The 17 uncovered lengths are exactly the 31·2ᵏ and 63·2ᵏ families — the only lengths at which leading radices 63, 992, 1008 and 4032 are ever used, and therefore precisely where every silent-wrong-residue defect found in this campaign lives. All 27 radix sets there fail today in plain AVX2.

What this changes

  • An exit contract: 1 = wrong answer, 2 = length untested, 0 = clean.
  • A new -s c coverage sweep over the uncovered lengths. It needs no new reference data: Res64 depends only on (exponent, iters), so each length borrows the nearest tabulated row. Verified — p=17551099 gives AB1180428ED65EE0 at 896K and at all 12 radsets of 1024K.
  • The anchor fix, zero-residue rejection, the resFlag fix, the dead-branch fix, an always-printed summary, and CI gating via ${PIPESTATUS[0]} so the tee no longer swallows the status. CI fails on status 1 only, since status 2 fires on today's 1K/2K.
  • The same rule for the monthly large/huge self-test workflow, selftest-large.yml (added since by CI: move the large self-test to its own monthly workflow, and add the huge self-test #294): fail on 1, pass on 2, in POSIX form in the FreeBSD job.

Plain cross-radix-set agreement was considered and measured insufficient — at 992K both radsets share leading radix 992 and both return 0000000000000000, so they agree with each other and are both wrong.

Validated failing-first

A test that cannot fail is the subject of this PR, so each check was demonstrated to fail before being demonstrated to pass:

case before after
radix992 (nosimd), broken — 8 wrong answers, Res64 Error, exit 1
radix992 with #179 — 992/1984/3968/7936/15872 all pass, 0 wrong
radix16 zero-cosine (AVX-512/SDE, -fft 8 -radset 1) pristine main exit 0 on ERR_CARRY exit 2; with #214's fix, correct Res64 and exit 0
-s t / -s s negative controls — 93/93 and 127/127, exit 0

Independently reproduced here before pushing: stock main -s tt exits 0 on the 4K/5K/7K/9K/10K warnings above; this branch exits 1 on the identical set.

Runtimes (avx2, 4 threads): tt 2.2 s, t 15 s, s 44 s, m 1493 s, and -s c ≈2–4 min once those lengths work.

Known blockers for a stricter gate, not fixed here

1K and 2K are unusable in a stock AVX2 build of main — 1K gives ROE ≈0.4993 on iteration 1, and 2K radset 0 hits ERR_ASSERT "radix < 16 does not support shifted residues" against the self-test's own random RES_SHIFT. That is why CI gates on status 1 rather than != 0. AVX-512 under SDE loses 8 of 15 teensy lengths. And -s never exercises Fermat-mod at all.

Summary by CodeRabbit

  • New Features

    • Added a self-test coverage mode that sweeps supported FFT lengths through the largest Large-tier length, including lengths without built-in reference data.
    • Self-tests now provide a completion summary and distinguish incorrect results from cases that could not be tested adequately.
    • Self-tests use available reference data and can select a reference for supported lengths without a table entry.
  • Bug Fixes

    • Self-tests detect zero residues and disagreements between radix sets without allowing later matches to erase earlier failures.
    • FFT configuration checks continue after an untestable length and report other failures after processing all lengths.
    • Automated test runs accept self-test status 2 while still failing on other error statuses.

@MarkRose MarkRose added the bug Something isn't working label Jul 30, 2026
MarkRose added a commit to MarkRose/Mlucas that referenced this pull request Jul 30, 2026
…gating

The Linux, macOS and Windows jobs enable `set -o pipefail`, and primesearch#216's
self-test loops pipe into tee. A self-test exiting 2 - "too few usable radix
sets", which the gate is written to tolerate - therefore propagated out of the
pipeline and, with `set -e`, ended the step before the PIPESTATUS check could
classify it. Observed on this branch: several threaded Linux jobs failed with
exit code 2 immediately after a self-test, no error message.

Turn pipefail off for the duration of those loops so the pipeline status is
tee's, and read the self-test's real status from PIPESTATUS[0] as intended.

Verified the idiom behaves as claimed: a command exiting 2 through `|& tee`
is tolerated and PIPESTATUS[0] still reports 2, while a 1 would set rc=1.

The same flaw is in primesearch#216 itself, which is where the loops come from.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
MarkRose added a commit to MarkRose/Mlucas that referenced this pull request Jul 30, 2026
primesearch#216 defines two self-test statuses - 1 for a wrong answer, 2 for a length
with too few usable radix sets - and the gates test for exactly 1. Everything
else passes, including a crash. That is not hypothetical: the macOS jobs on
the previous push aborted eight self-tests with

  Assertion '0' failed: Multithreading must be enabled in build to permit -cpu

and the gate waved all eight through with exit 134. The job only went red
later, when config-fermat.sh hit the same assertion under `bash -e`.

Accept the two documented statuses and fail on anything else. Verified against
exits 0, 1, 2 and 134.

Same flaw is in primesearch#216, where the gates come from.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
MarkRose added a commit to MarkRose/Mlucas that referenced this pull request Aug 10, 2026
Per review, use GitHub's built-in paths filter rather than a gate job. This
replaces the dorny/paths-filter 'changes' job and the fourteen per-job
'needs: changes' + if-expression pairs with seven lines on the trigger, so the
PR is now a net addition of 7 lines rather than 49, and it no longer touches
the parts of ci.yml that primesearch#103, primesearch#155 and primesearch#216 also edit.

A docs-only PR currently spends 65 jobs building and testing every platform for
nothing.

Filtering is on pull_request only: pushes to main and the monthly schedule
still run everything, so nothing is skipped on the authoritative branch or the
cron sweep. paths-ignore rather than paths, deliberately - a denylist keeps
running CI for anything unrecognised, where an allowlist would fail open to
skipping the moment someone adds a file type nobody thought to list.

GitHub skips the run only when *every* changed file matches, so a PR touching
both docs and source still runs the full matrix. Checked against the cases that
matter: docs-only skips; source, workflow, shell-script, help.txt, mixed
docs+source, and an unrecognised new file type all still run.

Dropped the separate 'shell' category and its ShellCheck gating: that job takes
a few seconds, so separating it saved nothing.

No branch protection is configured on main, so there are no required status
checks that a skipped run could leave permanently pending.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@MarkRose
MarkRose force-pushed the fix-selftest-blindness branch from 7599ecc to 315415f Compare September 5, 2026 20:22
@MarkRose

MarkRose commented Sep 5, 2026

Copy link
Copy Markdown
Member Author

Rebased onto current main to clear the merge conflict.

Nine conflicts, all in ci.yml, because #103 reworked every self-test block this PR gates. Rather than hand-merge two divergent versions nine times, I took main's job structure wholesale and re-applied the gating on top of it, so the CI half of this PR is now written against the loops that actually exist — including the -prp loops and the per-word native Mfactor runs that did not exist when this branch was written.

The gate is now checked end to end

Worth showing, because the first control I ran did not actually test anything.

stock build      ./Mlucas -s tt -> exit 2   ("2 FFT lengths had too few usable radix sets")
corrupted build  ./Mlucas -s tt -> exit 1   ("17 cases returned a wrong answer")

so the job stays green on 2 and goes red on 1, which is what the gate is for.

Getting there took two false negatives worth recording. Corrupting the 1K row proved nothing: that case halts on a roundoff error in an AVX2 build before any residue is compared. Corrupting nineteen larger rows then also proved nothing, because makemake.sh refuses to overwrite an existing obj_avx2/Mlucas and I had re-run the stale binary. Only after deleting the object directory did the corruption show up. Both failures looked exactly like "the gate works" from the outside.

Build OK, and the stock -s tt summary is unchanged from main apart from the new pass/fail block.

@MarkRose
MarkRose requested a review from tdulcet September 5, 2026 20:23
MarkRose added a commit to MarkRose/Mlucas that referenced this pull request Sep 6, 2026
Per review, use GitHub's built-in paths filter rather than a gate job. This
replaces the dorny/paths-filter 'changes' job and the fourteen per-job
'needs: changes' + if-expression pairs with seven lines on the trigger, so the
PR is now a net addition of 7 lines rather than 49, and it no longer touches
the parts of ci.yml that primesearch#103, primesearch#155 and primesearch#216 also edit.

A docs-only PR currently spends 65 jobs building and testing every platform for
nothing.

Filtering is on pull_request only: pushes to main and the monthly schedule
still run everything, so nothing is skipped on the authoritative branch or the
cron sweep. paths-ignore rather than paths, deliberately - a denylist keeps
running CI for anything unrecognised, where an allowlist would fail open to
skipping the moment someone adds a file type nobody thought to list.

GitHub skips the run only when *every* changed file matches, so a PR touching
both docs and source still runs the full matrix. Checked against the cases that
matter: docs-only skips; source, workflow, shell-script, help.txt, mixed
docs+source, and an unrecognised new file type all still run.

Dropped the separate 'shell' category and its ShellCheck gating: that job takes
a few seconds, so separating it saved nothing.

No branch protection is configured on main, so there are no required status
checks that a skipped run could leave permanently pending.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@MarkRose
MarkRose force-pushed the fix-selftest-blindness branch from 315415f to c47251b Compare September 20, 2026 04:28
MarkRose added a commit to MarkRose/Mlucas that referenced this pull request Sep 22, 2026
…rimesearch#296's order

Merging PR primesearch#296 - which moves the ASan ladder to after config-fermat.sh and the
Mfactor runs - with PR primesearch#216, which edited the ladder in its old position, left
ASan-macOS and ASan-Windows with both copies: primesearch#216's gated one before
config-fermat.sh and primesearch#296's ungated one after the Mfactor loop. Both jobs ran
the whole ladder twice, and the ungated second copy ends the job under set -e
on the status-2 the first copy was written to tolerate.

Keep one ladder, gated as in primesearch#216 and placed as in primesearch#296. ASan-Linux already
merged that way; this brings the other two into line with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@MarkRose

Copy link
Copy Markdown
Member Author

Two follow-up commits, both fixing defects in this PR's own gating.

The status was never examined. The pipeline form

time ./Mlucas -s $s -cpu "..." |& tee -a test.log
st=${PIPESTATUS[0]}

runs under set -e with pipefail, so a nonzero status ends the step on the pipeline itself and the st= line never executes. The whole point of capturing the status was to tolerate 2; instead every job that hit it died on the first tier of the ladder and ran none of the others. The status is now captured in a || st=${PIPESTATUS[0]} tail, which set -e does not apply to.

The gate was too narrow. if (( st == 1 )) passes everything else, including the statuses the self-test never returns. A SIGSEGV part-way through -s m (139) or an abort (134) counted as a clean ladder - I have one of each in the current logs. It is now st != 0 && st != 2, so 2 stays tolerated for the stock build's 1K/2K rejections and nothing else does.

config-fermat.sh. Callers invoke it as bash -e -o pipefail -- ./config-fermat.sh, so now that Mlucas returns a nonzero status the first affected length ends the script. On ARM that is F18 at 15K, and F19 through F29 were never configured and never tested - the run simply looked finished. It now carries on past status 2 and exits nonzero at the end for anything else.

@MarkRose
MarkRose force-pushed the fix-selftest-blindness branch from 76f51f8 to 0d68e2c Compare October 13, 2026 16:45
@MarkRose

Copy link
Copy Markdown
Member Author

Rebased onto main now that #291 is in. Besides the conflict resolutions, two things changed because of what #291 added:

  • A new commit gates four more self-test runs. Enabled -Wextra and updated CI to remove remaining error masking #291 added self-test runs to the Linux-32-bit and Linux old jobs and removed continue-on-error from both TSan jobs. All four ran the self-test bare under set -e, so with this PR they would have ended their jobs on status 2, which a stock build still returns at 1K and 2K. They now use the same gate as the other jobs: 0 and 2 pass, anything else fails.
  • The FreeBSD gate no longer uses a pipe. Enabled -Wextra and updated CI to remove remaining error masking #291 added FreeBSD 10 to the matrix, and its /bin/sh has no pipefail (the step only sets it for 11 and later). Piping Mlucas into tee there would report tee's status, so every run would pass. Mlucas now writes to a file, the status is taken directly, and the file is shown and appended to test.log.

I tested both gates against a stub Mlucas returning 0, 2, 1 and 139: the bash gates under bash -e, and the FreeBSD one under busybox sh without pipefail. 0 and 2 pass, 1 and 139 fail, and the output still reaches test.log.

MarkRose added a commit to MarkRose/Mlucas that referenced this pull request Oct 13, 2026
…cover the FFT lengths it never reached) into all-fixes-integration-v5

# Conflicts:
#	src/Mlucas.c
MarkRose added a commit to MarkRose/Mlucas that referenced this pull request Oct 14, 2026
…cover the FFT lengths it never reached) into all-fixes-integration-v5

# Conflicts:
#	src/Mlucas.c
@MarkRose
MarkRose force-pushed the fix-selftest-blindness branch from 0d68e2c to 02718b0 Compare October 17, 2026 01:31
@coderabbitai

coderabbitai Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review.

Recent review info
Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 0c590e5d-7196-412f-88ae-722c9651be51
Commits

Reviewing files that changed from the base of the PR and between ecc7f99 and c45526f.

Files selected for processing: 1
  • .github/workflows/ci.yml

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

Walkthrough

Mlucas adds coverage sweeps for FFT lengths without reference rows and distinguishes wrong answers from lengths that could not be tested. The configuration script and CI workflows capture and handle the resulting self-test exit statuses.

Changes: Self-Test Behavior

Layer / File(s) Summary
Residue checks and result accounting
src/Mlucas.c, help.txt
Self-tests detect incorrect residues, count test outcomes, and return status 1 for wrong answers or status 2 when an FFT length has too few usable radix sets. The help text documents the exit statuses.
Unlisted FFT-length coverage sweep
src/Mlucas.c, help.txt
The -s c, -s cover, and -s coverage options sweep supported FFT lengths without reference rows. Each length uses the nearest lower-or-equal table row for its exponent and reference residues.
Script and CI status handling
config-fermat.sh, .github/workflows/ci.yml, .github/workflows/selftest-large.yml
The configuration script continues after status 2 and returns other nonzero statuses after processing. CI self-test steps capture Mlucas statuses, accept 0 and 2, and fail for other statuses.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Bug fix

Possibly related issues

  • primesearch/Mlucas#224 — The issue connects this pull request to self-test coverage and status-gating changes.

Possibly related PRs

Suggested labels: enhancement, github_actions

Merge Risk: Minimal · up to c4552

The documented exit statuses now match how excessive-roundoff cases are handled. No actionable merge-blocking risk remains after normal checks.

Architecture risk: Low · up to faf88

The change affects 3 systems.

Changed systems: config-fermat.sh, help.txt, src

Architecture concerns
No architecture-level concerns identified.

Review details

Systems and components

  • observed — config-fermat.sh (service) was modified; 1 changed file maps to changed impact.
  • observed — help.txt (service) was modified; 1 changed file maps to changed impact.
  • observed — src (service) was modified; 1 changed file maps to changed impact.

Before / after behavior

  • observed — Modified behavior in help.txt: Adds -s cover/-s c for 100- or 1000-iteration coverage of supported FFT lengths lacking built-in reference-exponent rows, specifically the odd-31 and odd-63 families from 992K through 32M. Each length uses the exponent and stored residues of the nearest tabulated length at or below it; the stated runtime is roughly that of -s l.
  • observed — Modified behavior in help.txt: Documents self-test exit statuses: 0 if all radix sets match references or are skipped for a reason that does not impugn the answer; 1 if any case has a wrong answer, including a residue mismatch, disagreement between radix sets at one length, or an identically zero residue; and 2 if there are no wrong answers but a length has too few usable radix sets to test. A one-line summary is printed at the end.
  • observed — Modified behavior in src/Mlucas.c: Interactive self-tests now flag a residue as incorrect when all three Selfridge–Hurwitz residues are zero, except for a Mersenne primality test run for exactly (p-2) iterations. Unlike the removed comparison branches, matching any supplied reference residue no longer clears an existing failure; mismatches still set it.
  • observed — Modified behavior in src/Mlucas.c: The Res35m1 reference check now tests for inequality directly; a match no longer clears a failure recorded by an earlier check.
Pre-merge checks: Passed 4
Check name Status Explanation
Title check Passed The title names the two main changes in the PR: making the self-test able to fail (wrong answers return status 1, untested lengths return status 2) and extending coverage to FFT lengths the self-test …
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Full details: Title check

Explanation

The title names the two main changes in the PR: making the self-test able to fail (wrong answers return status 1, untested lengths return status 2) and extending coverage to FFT lengths the self-test never reached through the new -s c/cover sweep. The raw summary and PR objectives both confirm these as the primary changes. The title is 81 characters, which is slightly long but still a single descriptive sentence.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

MarkViafoura and others added 13 commits October 29, 2026 09:14
…ngths it never reached

The self-test could not report a failure. Five separate defects, each sufficient
on its own to make a wrong answer invisible:

1. main() returned 0 unconditionally from the -s/-iters path, including right
   after printing "WARNING: 0 of N radix-sets at FFT length L K passed - PLEASE
   CHECK YOUR BUILD OPTIONS". The four CI self-test steps invoke `Mlucas -s
   <tier>` and rely on its exit status, so no self-test regression could ever
   turn CI red. On an AVX2 build of the current tree `-s tt` reports 0 of 2
   radix sets passing at both 1K and 2K and still exits 0.
   Now: exit 1 if any case returned a demonstrably wrong answer, 2 if no wrong
   answers but some FFT length had too few usable radix sets to be tested at
   all, 0 otherwise. Splitting the two lets a caller gate on wrong answers alone
   on a host with a known-marginal FFT length.

2. The new-data consensus anchor test was (!new_res.sh0), which conflates "no
   anchor yet" with "the anchor is 0". If the first radix set returned Res64 = 0
   - the signature of the radix1008/4032 and AVX-512 radix1024 silent-wrong-
   answer defects - every subsequent radix set re-took the "first" arm and was
   counted as having succeeded whatever it computed, and a .cfg entry asserting
   a 0 consensus residue was written. Anchor on the count of radix sets which
   have reached that point instead. This path covers all 42 reference-free rows
   of MvecPRP[], the 10000-iter column of the 17 largest MersVec[] rows, and
   every FFT length absent from the tables.

3. The Selfridge-Hurwitz comparisons in ernstMain's timing-test block set
   resFlag = 0 on a match, so a Res64 mismatch was erased by a subsequent
   Res35m1 match. Only ever set it.

4. Nothing rejected an identically-zero residue when no reference was stored -
   it was silently adopted as the new reference. The roundoff-error check cannot
   see this: fracmax measures distance from the nearest integer and zero is an
   integer, so a zeroed residue reports AvgMaxErr = MaxErr = 0. Reject it, with
   an exemption for the one case where zero is the correct answer (an LL test
   run to exactly p-2 iterations).

5. The "FFT length not in the reference table" test in the -fft handler was
   (i != MvecPtr[j].fftLength) evaluated at j == numTest, i.e. against the
   scratch row into which the -fft argument had just been copied - never true.
   The auto-pick-an-exponent branch behind it was therefore dead and `-fft L
   -iters N` for an off-table L exited with "nor user-supplied command-line
   exponent" instead of running. Test j == numTest, as the equivalent loop 30
   lines above already does.

Plus new '-s c': a coverage sweep over the supported FFT lengths that have no
reference-table row. Every -s tier steps through rows of MersVec[]/MvecPRP[], so
the lengths those tables list are the only ones the self-test can reach. A SIMD
build supports 153 FFT lengths / 603 (length, radix set) pairs; the tables reach
136 / 564. The 17 missing lengths are exactly the odd-31 and odd-63 families
(992, 1008, 1984, 2016, ... 516096), and they are the only place leading radices
63, 992, 1008 and 4032 are ever used - 4 of the 34 leading radices a SIMD build
can select had no self-test coverage of any kind. The sweep needs no new
reference data: the residue triplet after a fixed number of iterations depends
only on the exponent and the iteration count, not on the FFT length, so an
uncovered length can borrow both the exponent and the stored residues of the
nearest table row at or below it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ate on the status

Follow-on to the previous commit:

o Print a one-line "N of M (FFT length, radix set) cases ran and gave the
  expected residue" summary at the end of every self-test, pass or fail. Without
  it the only record of a radix set having been skipped is a line buried
  thousands of lines up in the log - which is how the fact that a stock AVX2
  build fails every radix set at both 1K and 2K went unremarked. The counts
  cross-check against an independent enumeration of get_fft_radices(): -s tt =
  26 cases, -s t = 93, -s s = 127 in an AVX2 build.

o help.txt: document -s c and the exit-status contract.

o ci.yml: the four self-test steps invoked `Mlucas -s <tier>` and relied on its
  exit status, which was always 0; three of them additionally piped into tee,
  whose status is the one bash reports for a pipeline, so even a fixed exit
  status would have been discarded. Capture the real status via PIPESTATUS and
  fail the job on status 1 (a wrong answer). Status 2 (an FFT length with too
  few usable radix sets) is tolerated for now because it fires on a stock build
  at 1K and 2K; the comment says to tighten to 0 once that is fixed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two defects in the gating this PR added.

The pipeline form - 'time ./Mlucas -s $s |& tee -a test.log' followed by
'st=${PIPESTATUS[0]}' - runs under set -e with pipefail, so a nonzero status
ends the step on the pipeline itself and the next line never executes. The
whole point of capturing the status was to tolerate 2; instead every job that
hit it died on the first tier of the ladder, before running any of the others.
Capture the status in a '|| st=${PIPESTATUS[0]}' tail, which set -e does not
apply to.

The gate itself was 'st == 1', which passes everything else - including the
statuses the self-test never returns. A SIGSEGV part-way through '-s m' (139)
or an abort (134) counted as a clean ladder. Gate on 'st != 0 && st != 2', so
2 stays tolerated for the stock build's 1K/2K rejections and nothing else does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Callers run this as 'bash -e -o pipefail -- ./config-fermat.sh', so now that
Mlucas returns a nonzero status the first affected length ends the script. On
ARM that is F18 at 15K, three of whose four radix sets decline Fermat-mod, so
F19 through F29 were never configured and never tested - the run looked like it
had simply finished.

Carry on past status 2, which says only that this build could not write a cfg
entry for that length, and exit nonzero at the end for anything else.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
vmactions/freebsd-vm runs with usesh: true, so that step is /bin/sh, not bash.
${PIPESTATUS[0]} and (( ... )) are bashisms:

    sh: ${PIPESTATUS[...}: Bad substitution
    ##[error]Process completed with exit code 2.

The gate never worked there - previously set -e killed the step on the pipeline
before either bashism was reached, so the breakage was invisible.

'set -o pipefail' is already set at the top of the step and tee always succeeds,
so the pipeline already carries Mlucas's own status and a plain '|| st=$?'
captures it. The arithmetic tests become [ ... ] -ne / -eq.

Checked against a stub binary under a POSIX shell: status 0 and 2 pass the gate,
1 and 139 fail it, and the block parses under ash.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…new exit status

and removed continue-on-error from the two TSan jobs. All four run the
self-test bare under set -e, so with this PR's exit status they would end
their jobs on status 2, which a stock build still returns at 1K and 2K.
Gate them like the other jobs: 0 and 2 pass, anything else - a wrong
answer, a crash - fails.

(The rebase onto primesearch#291 also changed the FreeBSD gate: primesearch#291 added FreeBSD
10, whose /bin/sh has no pipefail, so that gate no longer pipes Mlucas
into tee. It writes to a file, takes the status directly, and then shows
the file and appends it to test.log.)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…est workflow

selftest-large.yml (from primesearch#294) runs '-s l' and '-s h' in plain loops under pipefail, which is
right while the self-test always exits 0. With this PR it exits 1 for a wrong answer and 2 when
some FFT length had too few usable radix sets, so give those loops the same rule as ci.yml: fail
on 1, pass on 2. The FreeBSD job uses the POSIX form, as in ci.yml.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…under a plain 'bash' too

Without pipefail the status of the Mlucas | tee | grep pipeline is grep's, and grep almost always
matches a line of Mlucas's output, so '|| st=${PIPESTATUS[0]}' never ran and a wrong answer or a
crash passed. CI invokes the script with pipefail, but the documented invocation does not.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rebased onto main, where the TSan jobs tee their self-test output into
test.log, so their gate reads the self-test's status from PIPESTATUS.
Say so in the comment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fftLength is an int and len a uint32, which drew -Wsign-compare now
that main builds with -Wextra. Table lengths are positive, so the cast
changes nothing but the warning.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each -s argument rewrites start/finish, so '-s t -s c' silently dropped
the tiny tier and ran only the coverage sweep, and '-s c -s t' left
start at the tiny tier's offset while the sweep set finish = nsweep,
so no case ran and the run reported 0 of 0 with exit status 0. The
same happened with '-s c -fft 992 -iters 100', where the -fft handling
leaves start = numTest. -m and -f point start/finish at the single-case
slot too, and -f switches the modulus to Fermat, which the sweep does
not support.

Allow only one -s per run, and refuse -s c together with -fft, -radset,
-m or -f with an explicit message and a nonzero exit, instead of running
nothing and reporting success. Assert start == 0 when the sweep sets
finish, so a sweep that would run zero cases can no longer pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
primesearch#365 moved the single-threaded jobs' self-tests into parallel steps. Apply
the same rule as the other jobs there: 0 and 2 pass, anything else fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1ceddcf replaced the TSan self-test lines with the exit-status gate and
took the upload-artifact step after each with them. Put both back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MarkRose
MarkRose force-pushed the fix-selftest-blindness branch from ecc7f99 to c45526f Compare October 29, 2026 15:18
@coderabbitai coderabbitai Bot added enhancement New feature or request github_actions Pull requests that update GitHub Actions code labels Oct 29, 2026
@MarkRose

Copy link
Copy Markdown
Member Author

I rebased this onto main, only to fix the conflicts in .github/workflows/ci.yml with #365 and #367, so this needs your approval again. Two small commits come with it:

Everything else is unchanged.

@MarkRose

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Oct 31, 2026 •

Copy link
Copy Markdown
Action not completed

Already reviewed the last commit. Use @coderabbitai full review to rerun a review of the entire changeset.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working enhancement New feature or request github_actions Pull requests that update GitHub Actions code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants