Skip to content

test(fuzz): cover native producer boundaries - #22

Draft
dangduc wants to merge 5 commits into
jojojames:mainfrom
dangduc:codex/fzfa-fuzz-native-producers
Draft

test(fuzz): cover native producer boundaries#22
dangduc wants to merge 5 commits into
jojojames:mainfrom
dangduc:codex/fzfa-fuzz-native-producers

Conversation

@dangduc

@dangduc dangduc commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

What this adds

This is stack PR 2 of 3. It sends generated command output through fzfa and the
real fzf-native 2.7 session API. It also tests the command assembled by
fzfa-ugrep.

There are no runtime changes in this PR.

This PR is based on #21. Until #21 merges, GitHub's full diff includes the
earlier state harness. Review this PR's incremental change here:

dangduc/fzfa@codex/fzfa-fuzz-state-models...codex/fzfa-fuzz-native-producers

What one producer case does

Each case starts a real child process, lets fzfa submit and read a real native
request, records every publishable result, waits for the terminal snapshot,
redraws three more times, and stops the source.

Generated byte streams include:

  • normal LF and Windows CRLF rows;
  • ANSI color and an ANSI sequence cut off at end of file;
  • café, 你好, and raw invalid UTF-8 bytes;
  • duplicate and empty rows;
  • long rows;
  • a useful prefix followed by a NUL byte;
  • a useful prefix followed by process exit status 7; and
  • output split across two real writes with a pause between them.

Simple examples

A bad tail must not erase a good prefix

The child writes these bytes:

valid\n
ab<NUL>cd\n
late\n

The expected result is:

  • valid remains available because it was a complete row before the error;
  • neither ab nor late reaches Emacs as a candidate;
  • the native status is terminal;
  • fzfa reports the producer failure once; and
  • three later redraws return the same candidates and counts without reporting
    the error again.

Every interim and terminal final value is checked for a proper list of
strings and for forbidden NULs. A self-test injects a NUL only into an interim
publication, then allows the terminal value to be clean. The oracle must still
reject the run.

A real stream must publish before it finishes

One fixed child process writes:

first\n
<pause 0.3 seconds>
second\n

The harness requires a publishable ("first") observation before the final
("first" "second") observation. This checks a growing stream, not only a
fully buffered process result.

The line limit has three different meanings

For a limit of 4, the fixed rows have lengths 3, 4, and 5:

aaa
bbbb
ccccc
  • Ambient positive 4: keep aaa and bbbb; exclude ccccc.
  • Explicit negative -4: keep aaa and bbbb; truncate the last row to
    cccc.
  • Explicit nil: keep all three rows without a limit.

If a case omits :max-line-length, the harness now preserves the ambient fzfa
setting. A controlled mutation recreates the old harness mistake—silently
changing an omitted value to nil—and must be rejected.

The ugrep test needs proof that search worked

The generated directory looks like this:

normal.txt       contains a unique visible sentinel
manual.info      contains a late NUL
manual.info-1    contains a late NUL
emms/cache       contains a late NUL
late.bin         contains a late NUL

The test first requires all positive evidence:

  • ugrep exits with status 0;
  • raw stdout contains the exact normal.txt sentinel; and
  • the final native candidates contain the same sentinel.

Only then does it accept the absence of the excluded Info and EMMS paths as
evidence. late.bin is not an explicit fzfa exclusion: ugrep may omit it as
binary, or the native seam must reject it if its NUL reaches the pipe.

This prevents an empty or unrelated command from passing every negative
assertion.

Oracle qualification

make producer-selftest first checks that seeds 1 through 200 reach all ten
byte-stream categories. It then requires five controlled defects to fail:

  1. a NUL in an interim candidate;
  2. loss of the ambient line cap;
  3. a changed count on a stable redraw;
  4. a missing ugrep sentinel in raw stdout; and
  5. a missing ugrep sentinel in final candidates.

This qualifies FZFA-C11, C12, and C13 in the contract catalog. C07 remains
partial because its “report exactly once” assertion does not yet have its own
controlled mutation.

CI and local use

Pull requests run 100 generated producer cases. The weekly job runs 1,000. CI
installs ugrep and runs the tool boundary after the producer lane.

make -C fuzz producer-selftest
make -C fuzz producer
make -C fuzz tools
FZFA_FUZZ_SEED=123 make -C fuzz producer CASES=1

Python 3 is required. make tools prints a skip when ugrep is not installed.

Verification performed

  • fuzz harness byte compilation with warnings treated as errors;
  • five controlled producer/tool defects rejected by their intended oracles;
  • the fixed two-chunk stream and three line-cap boundary cases;
  • 300 generated native producer cases, root seed 1;
  • three stable redraw checks and native-handle teardown per case; and
  • the full aggregate repository suite: 261 expected, 0 unexpected.

The local tool run skipped because ugrep is not installed. The Linux CI job is
the ugrep execution gate.

Stack

@dangduc

dangduc commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator Author

I ran a two-round adversarial review of the new fuzz oracles using six independent, persona-inspired review lenses. Each requested item below was reproduced with a temporary local mutation and then challenged by another reviewer. These are harness false positives, not claims that the current production code is broken.

Requested before merge

1. Exercise the real line policy and validate every publishable result

fzfa-fuzz-producer--run-command (fuzz/fzfa-fuzz-producer.el:95-173) binds fzfa-max-line-length to nil whenever a spec omits :max-line-length. Every generated spec omits it, so the long cases bypass the normal default of 256 and expect 300-1999 byte rows unchanged. A mutation that disabled the production fzfa-max-line-length bridge passed 120 generated cases.

The wait loop also overwrites each result and validates only the reader-done terminal value. That is observable, not hypothetical: real delayed producers returned growing, publishable final candidate sets before reader-done; one measured run returned 69 such values. A wrapper injected a NUL-bearing interim final, followed by a clean terminal result, and the harness still passed. The new stable-redraw loop also checks only the final tag. Existing ERT already catches a changed stable result, so the unique coverage hole here is the unchecked interim output; complete equality in this loop would still keep its own assertion meaningful.

Please:

  • Preserve the ambient fzfa-max-line-length when the key is absent; use an explicit :max-line-length nil only for intentionally unlimited cases.
  • Add positive and negative cap cases around N-1/N/N+1.
  • Check every returned final for a proper candidate list and forbidden NULs.
  • After reader completion, compare each stable redraw's complete result—candidates and counts, not only its tag—with the terminal result.

Also narrow the file commentary: NUL-bearing output and nonzero exits become visible failures, while invalid UTF-8 bytes are intentionally retained.

2. Give the ugrep test a positive control

The fixture writes normal.txt containing visible-alpha, but fzfa-fuzz-tools-batch (fuzz/fzfa-fuzz-producer.el:258-321) never requires that record in raw output or native candidates. The target passed both an empty successful command and unrelated garbage output whose shell comment merely contained the word ugrep.

Please require exit status 0 and the exact normal-file sentinel in both raw stdout and the final candidate list before checking that excluded and NUL-bearing files stayed out.

Nonblocking follow-up

Seeds 1-10 already reach all ten producer categories, and much of the parser combinatorics exists in fzf-native's ERT/C/session fuzz suites. After the adapter assertions above are explicit, consider keeping a small fzfa boundary matrix and leaving high-volume parser variation to fzf-native.

Baseline remained green before mutations: 261/261 fzfa ERT tests and producer runs through 1,000 generated cases. The isolated worktree was restored and verified clean after every probe.

@dangduc
dangduc force-pushed the codex/fzfa-fuzz-native-producers branch from 315f178 to 2052729 Compare September 3, 2026 09:14
@dangduc
dangduc force-pushed the codex/fzfa-fuzz-native-producers branch from 2052729 to 1535033 Compare September 3, 2026 13:24
@dangduc

dangduc commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator Author

Phase-two oracle work is now on this branch at 1535033.

The requested findings are addressed:

  • An absent :max-line-length now preserves ambient fzfa policy. Fixed rows at
    N-1, N, and N+1 distinguish positive exclusion, negative truncation, and an
    explicit unlimited value.
  • Every publishable final value is checked for shape, strings, and NULs. A
    real two-write child must publish ("first") before
    ("first" "second").
  • Stable redraws must equal the complete terminal value, including candidates
    and counts.
  • The ugrep integration requires exit status 0 and an exact unique sentinel in
    both raw stdout and final native candidates before it checks exclusions.
  • Seeds 1 through 200 must reach all ten byte-stream categories.

make producer-selftest rejects five controlled defects, including the old
ambient-cap mistake, an interim NUL, a changed redraw count, and each missing
ugrep positive observation. Compile and 300 generated native cases pass. The
local tools target skips because ugrep is absent; this PR's Linux
Native producer and tool boundary CI job, which installs ugrep, passes.

FZFA-C07 remains partial because the “report once” assertion does not yet have
its own controlled mutation. The PR body now calls out that boundary.

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