Synopsis:
This page covers the four suites and what they leave uncovered. Three unit suites for
the three libraries, one integration suite driving the built executable.
-
88 tests, which do not compare with the C++ page's 151 assertions - a Rust test holds
as many assertions as its claim needs, so the difference is one of unit rather than
of coverage.
-
This is the first implementation written against the verification requirement rather
than found to satisfy it, that requirement having come out of the Rust audit.
No test framework, and in Rust that costs nothing.
#[test], assert_eq!, and cargo test ship with the language, where C++ writes a Checker struct into each of four test modules to get the same three things.
- Unit suites live inside the crate so they can reach private items; the integration suite lives outside so it sees the binary, whose path Cargo supplies as a compile-time literal.
- The Output suite depends on the harness running each test on its own thread, since the one-sink flag is a
thread_local!.
Every suite passes, and Section 6 names four claims that are read rather than checked.
- The unrenderable name - the rule that cost two implementations a code change is tested in neither.
- The failed write, and both paths to exit code 2, none of which a test can provoke.
- The POSIX side of everything, which a second machine would close without a line of new test code.
1. Four Suites
Three unit suites cover the three libraries and one integration suite covers the binary by
driving the built executable end to end. The division follows from what
rust_textfinder_entry is: a binary whose behavior is its startup sequence, its
exit codes, and its stream routing, none of which can be exercised without running it.
That division is a requirement rather than a habit. Spec_TextFinder.md §6.2 asks every
implementation for unit suites one per library component, one integration suite driving the
built executable, one demonstration, and a runner per kind that announces each suite and
exits with the number that failed. §6.2 arrived out of the Rust audit, so this is the
first implementation written against it rather than found to satisfy it; the
Contracts page
covers what it asks of the two implementations still to come.
| Suite |
Tests |
What it covers |
| Cmdline |
29 |
Every default, both introducers, boolean case folding, last-occurrence-wins, /P accumulation, the /p normalization rules including the six trimmed characters, all six parse diagnostics, and the rendered help and option listings |
| Dirnav |
33 |
Skip-list pruning including the root carve-out, /p selection with the dot-file rule, /s false, all four block forms, the /h gating, cannot open, LF, CRLF and bare CR splitting, BOM consumption, and the 10 MB limit |
| Output |
4 |
The one-sink rule, the release of that right on drop, both write methods reporting nothing and panicking on nothing, and dispatch through the Output trait |
| Integration |
22 |
The executable end to end: exit codes, stream routing, /H, the bare command line, the /v ordering, LF-only output, separator normalization, and all seven usage diagnostics |
88 tests in total. A Rust test is one #[test] function holding as many
assertions as the claim needs, so these counts are not comparable with the C++ suites'
assertion counts. The
C++ Testing page reports
151 assertions over the same ground, and the difference is one of unit rather than of
coverage: every_parse_failure_reaches_stderr_with_exit_code_one is one test and
18 assertions, three for each of the six reason lines.
Two suites deserve a note on what they prove beyond their counts. The Dirnav suite builds a
temp tree and supplies its own Recorder, a struct holding one
Vec<String> that implements Output, which satisfies the same
bound the real sink does and so demonstrates that the generic parameter binds to any
implementation of the trait. The Output suite cannot read stdout back, since the sink owns
the only handle to it, so it checks what does not depend on reading the stream and leaves
the bytes to the integration suite.
The Dirnav and integration suites sort emitted lines before comparing wherever more than one
file is involved, because Spec_TextFinder.md §3.2 leaves the order of a directory's
entries to the filesystem. A suite that compared unsorted output would be asserting a
property the specification declines to guarantee.
2. The Harness
No test framework, and in Rust that costs nothing: #[test],
assert_eq!, and cargo test ship with the language, so
Spec_TextFinder.md §6's confinement to the standard library leaves the suites with a
working runner, per-test isolation, and a failure report that prints both sides of a
mismatch. The C++ implementation writes a Checker struct in each of its four
test modules to get the same three things.
Each unit suite sits beside the code it tests, as a src/unit_tests.rs the crate
root declares:
#[cfg(test)]
mod unit_tests;
The module compiles only under cargo test, so nothing of a suite reaches the
shipped library, and use super::*; at its top gives it the crate's private
items. That is what lets the Output suite read SINK_TAKEN directly and the
Dirnav suite call normalize, neither of which is pub. A
tests/ directory would compile against the public interface alone and could
reach neither.
The integration suite is a tests/integration.rs for that reason reversed: it
must see the binary rather than the library, and Cargo hands it the built executable's path
as an environment variable at compile time.
const EXECUTABLE: &str = env!("CARGO_BIN_EXE_rust_textfinder");
env! resolves at compile time, so the constant is a literal and the suite
cannot run against a path that does not exist. The C++ integration suite gets the same
string through a CMake generator expression and a target_compile_definitions
call; Cargo supplies it because the test target and the binary target are in one package.
Both the Dirnav and integration suites build temp trees through a TempTree
helper whose Drop removes the directory, so a failing test leaves nothing
behind. Each tree's name carries the process id and an
AtomicUsize counter, which is what keeps two trees apart when the harness runs
the tests in parallel.
That parallelism is worth one more sentence, because the Output suite depends on it. Cargo
runs each #[test] on its own thread, and SINK_TAKEN is a
thread_local!, so each test sees a fresh flag and the four sink tests do not
interfere. A plain static would have made them order-dependent, which is the
second argument for the choice Spec_Rust_TextFinder_Output.md §4 makes on other
grounds.
3. The Runners
Three batch files sit at the top of the Rust project. Each builds the workspace first,
brackets every suite with a === starting ... banner and a
=== ... returned N line, then prints a summary and returns the number of failed
suites as its own exit code.
| Runner |
What it does |
run_unit_tests.bat |
Runs the Cmdline, Dirnav, and Output suites in turn with cargo test -p <package> --lib |
run_integration_tests.bat |
Runs cargo test -p rust_textfinder_entry --test integration |
run_demo.bat |
Runs the demonstration with --nocapture and supplies the capture date through TEXTFINDER_DEMO_DATE |
Each takes its working directory from its own location with cd /d "%~dp0", so
any of the three runs from any working directory. A failed build stops before running a
suite and says that every suite counts as failed, which satisfies §6.2's rule that a
suite never built counts as a failure rather than being skipped silently.
Each builds the workspace before its first suite, which is what §6.2 asks of a runner
so that a fresh checkout needs no separate step and a reader cannot run yesterday's binary
against today's source. Rust needs no work to satisfy that: cargo build is
already incremental, so a run after a build costs a fraction of a second and one after an
edit rebuilds only what changed. The
C++ runners pay more for
the same rule, having to configure CMake when there is no cache and to establish the MSVC
environment when the shell lacks it.
Each also holds the console after its summary, so a reader who starts one from a file
manager sees its output rather than a window that closes. The hold is one
:hold subroutine, it comes last, and it does not change the exit code - a
pause leaves ERRORLEVEL untouched, and the runner returns its
failure count explicitly in any case. Setting TEXTFINDER_NO_PAUSE to a
non-empty value suppresses it, which is how the captures in Sections 4 and 5 were taken and
how the demonstration of §7.5 is recaptured.
The --lib flag on the unit runs is what keeps the three suites separate.
Without it cargo test -p rust_textfinder_entry would also build and run the
integration and demonstration targets, and the unit runner would report a number that
included them.
run_demo.bat passes the local date in rather than letting the demonstration
compute it. The demonstration falls back to a civil date derived from the UTC clock when the
variable is absent, which reads as the previous day late in the evening, so the runner is
the authority for the date a capture carries.
run_unit_tests.bat
@echo off
rem run_unit_tests.bat - runs every rust_textfinder unit suite and reports each status
rem
rem Spec_TextFinder.md §6.2: the runner builds what it is about to run, announces each
rem suite and the status it returned, exits with the number of suites that failed, and
rem holds the console after its summary.
setlocal enabledelayedexpansion
cd /d "%~dp0"
set FAILURES=0
echo building the workspace
cargo build --workspace --quiet
if errorlevel 1 (
echo build failed; all 3 unit suites count as failed
set FAILURES=3
goto :summary
)
call :suite rust_textfinder_cmdline
call :suite rust_textfinder_dirnav
call :suite rust_textfinder_output
:summary
echo.
echo !FAILURES! unit suite^(s^) failed
call :hold
exit /b !FAILURES!
:suite
echo.
echo === starting unit suite: %1
cargo test -p %1 --lib
set STATUS=!errorlevel!
echo === unit suite %1 returned !STATUS!
if not "!STATUS!"=="0" set /a FAILURES+=1
exit /b 0
rem --- §6.2: hold the console unless a capture suppressed it. ---
:hold
if not "%TEXTFINDER_NO_PAUSE%"=="" exit /b 0
echo.
pause
exit /b 0
run_integration_tests.bat
@echo off
rem run_integration_tests.bat - runs the rust_textfinder integration suite and reports its status
rem
rem Spec_TextFinder.md §6.2: the runner builds what it is about to run, announces the
rem suite and the status it returned, exits with the number of suites that failed, and
rem holds the console after its summary.
setlocal enabledelayedexpansion
cd /d "%~dp0"
set FAILURES=0
echo building the workspace
cargo build --workspace --quiet
if errorlevel 1 (
echo build failed; the integration suite counts as failed
set FAILURES=1
goto :summary
)
echo.
echo === starting integration suite: integration
cargo test -p rust_textfinder_entry --test integration
set STATUS=!errorlevel!
echo === integration suite integration returned !STATUS!
if not "!STATUS!"=="0" set /a FAILURES+=1
:summary
echo.
echo !FAILURES! integration suite^(s^) failed
call :hold
exit /b !FAILURES!
rem --- §6.2: hold the console unless a capture suppressed it. ---
:hold
if not "%TEXTFINDER_NO_PAUSE%"=="" exit /b 0
echo.
pause
exit /b 0
4. Unit Suite Output
Captured 2026-09-16 from run_unit_tests.bat. Each suite names every test,
prints its count, and ends with the line the harness writes. The test names are the
specification restated as claims, which is what makes a passing run readable as coverage
rather than only as a number.
The order the names appear in is the order the tests finished, not the order they are
written, since the harness runs them in parallel. A reader looking for a particular claim
reads the list rather than scanning it in source order.
=== starting unit suite: rust_textfinder_cmdline
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.02s
Running unittests src\lib.rs (target\debug\deps\rust_textfinder_cmdline-96a227066ace06c9.exe)
running 29 tests
test unit_tests::a_boolean_switch_rejects_any_other_value ... ok
test unit_tests::a_token_without_an_introducer_is_not_a_switch ... ok
test unit_tests::an_argument_is_consumed_verbatim_even_when_it_looks_like_a_switch ... ok
test unit_tests::an_undefined_introducer_led_token_is_unrecognized ... ok
test unit_tests::a_trailing_switch_is_missing_its_argument ... ok
test unit_tests::booleans_are_matched_case_insensitively ... ok
test unit_tests::both_introducers_are_equivalent ... ok
test unit_tests::duplicate_extensions_are_retained_and_order_preserved ... ok
test unit_tests::empty_arguments_are_rejected_for_root_path_and_expression ... ok
test unit_tests::empty_command_line_yields_the_defaults ... ok
test unit_tests::every_diagnostic_ends_with_the_usage_line ... ok
test unit_tests::every_returned_string_ends_with_a_newline ... ok
test unit_tests::extensions_are_split_trimmed_and_stripped_of_one_dot ... ok
test unit_tests::extension_case_is_not_folded_by_the_parser ... ok
test unit_tests::first_root_path_replaces_the_default_even_when_equal_to_it ... ok
test unit_tests::last_occurrence_wins_for_every_other_switch ... ok
test unit_tests::no_line_of_the_option_listing_ends_in_whitespace ... ok
test unit_tests::empty_extension_items_are_discarded ... ok
test unit_tests::only_one_leading_dot_is_stripped ... ok
test unit_tests::help_text_carries_every_switch_and_ends_with_a_newline ... ok
test unit_tests::option_listing_emits_one_line_per_root_and_a_joined_extension_list ... ok
test unit_tests::option_listing_of_a_bare_v_true_command_line ... ok
test unit_tests::option_listing_of_the_defaults_reads_v_false ... ok
test unit_tests::parsing_stops_at_the_first_violation ... ok
test unit_tests::program_name_is_not_inspected ... ok
test unit_tests::repeated_root_paths_accumulate_in_order ... ok
test unit_tests::switch_letters_are_case_sensitive ... ok
test unit_tests::the_six_named_characters_are_trimmed_and_no_others ... ok
test unit_tests::usage_line_is_the_first_line_of_the_help_text ... ok
test result: ok. 29 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
=== unit suite rust_textfinder_cmdline returned 0
The 29 Cmdline tests divide three ways: the defaults against Spec_TextFinder.md §5, the
four parsing rules and the six diagnostics this library owns, and the rendered text against
§5.1 and §5.3. Two names carry the audit findings of the
Process page as claims:
the_six_named_characters_are_trimmed_and_no_others is the whitespace rule that
moved up to the parent specification, and
first_root_path_replaces_the_default_even_when_equal_to_it is the
-P . case that rules out inferring the first occurrence from the vector.
=== starting unit suite: rust_textfinder_dirnav
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.03s
Running unittests src\lib.rs (target\debug\deps\rust_textfinder_dirnav-aee1a9ce695c7482.exe)
running 33 tests
test unit_tests::a_malformed_expression_is_returned_rather_than_panicking ... ok
test unit_tests::a_root_path_that_cannot_be_opened_is_announced ... ok
test unit_tests::an_error_announcement_is_not_gated_on_h ... ok
test unit_tests::a_block_writes_its_path_once_however_many_lines_match ... ok
test unit_tests::a_root_named_in_the_skip_list_is_traversed_and_pruning_resumes_below_it ... ok
test unit_tests::a_matching_line_yields_one_detail_line_however_many_occurrences_it_holds ... ok
test unit_tests::line_iteration_treats_a_final_unterminated_run_as_a_line ... ok
test unit_tests::a_root_path_that_is_a_regular_file_is_searched_and_filtered_like_any_other ... ok
test unit_tests::a_leading_bom_does_not_belong_to_the_first_line ... ok
test unit_tests::a_detail_line_carries_only_the_fields_n_and_l_select ... ok
test unit_tests::a_searched_file_that_matched_nothing_is_announced_only_under_h_false ... ok
test unit_tests::path_joining_never_doubles_a_separator ... ok
test unit_tests::a_skip_list_directory_is_pruned_silently ... ok
test unit_tests::evaluation_stops_at_the_first_match_when_neither_n_nor_l_is_set ... ok
test unit_tests::an_empty_extension_list_selects_every_file ... ok
test unit_tests::a_skipped_file_is_silent_under_the_default_h ... ok
test unit_tests::a_file_holding_a_nul_byte_or_invalid_utf8_is_skipped ... ok
test unit_tests::a_file_above_the_size_limit_is_never_read ... ok
test unit_tests::a_pruned_directory_is_counted_as_neither ... ok
test unit_tests::a_root_that_is_a_regular_file_counts_as_a_file_and_neither_noun_inflects ... ok
test unit_tests::default_command_line_reports_every_nonempty_file_without_opening_it ... ok
test unit_tests::a_root_that_cannot_be_opened_is_counted_as_neither ... ok
test unit_tests::every_path_begins_with_the_root_and_uses_forward_separators ... ok
test unit_tests::lf_crlf_and_bare_cr_each_terminate_a_line ... ok
test unit_tests::line_numbers_count_lines_that_did_not_match ... ok
test unit_tests::one_navigator_serves_every_root_path ... ok
test unit_tests::a_file_the_extension_list_excluded_is_not_counted ... ok
test unit_tests::recursion_off_searches_the_root_directory_alone ... ok
test unit_tests::every_examined_file_and_every_entered_directory_is_counted ... ok
test unit_tests::a_non_empty_extension_list_selects_by_last_dot_suffix ... ok
test unit_tests::under_recursion_off_no_subdirectory_is_counted ... ok
test unit_tests::the_counts_are_of_the_whole_run_and_an_entry_under_two_roots_counts_twice ... ok
test unit_tests::the_summary_is_not_gated_on_h ... ok
test result: ok. 33 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.03s
=== unit suite rust_textfinder_dirnav returned 0
The Dirnav suite tests the accepted costs rather than only the rules.
default_command_line_reports_every_nonempty_file_without_opening_it is
Spec_TextFinder.md §3.3's no-content case with both of its costs in one claim: a binary
file is reported, and an empty one is not.
a_root_named_in_the_skip_list_is_traversed_and_pruning_resumes_below_it is the
§3.2 carve-out and its limit in one name, and
every_path_begins_with_the_root_and_uses_forward_separators is the
normalization gap the audit found.
Eight of its 33 tests cover the run summary of §3.6, and they read as that section's
exclusion list turned into claims:
a_file_the_extension_list_excluded_is_not_counted,
a_pruned_directory_is_counted_as_neither,
under_recursion_off_no_subdirectory_is_counted,
a_root_that_cannot_be_opened_is_counted_as_neither.
the_counts_are_of_the_whole_run_and_an_entry_under_two_roots_counts_twice
drives one navigator over two roots the way the binary does, which is the only way to test
that the counts are never reset between them. And
a_root_that_is_a_regular_file_counts_as_a_file_and_neither_noun_inflects
asserts accessed 1 files, 0 directories verbatim, pinning the uninflected
singular §3.6 accepts rather than leaving a later reader to correct it.
=== starting unit suite: rust_textfinder_output
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.03s
Running unittests src\lib.rs (target\debug\deps\rust_textfinder_output-828f4f4650b5899b.exe)
running 4 tests
test unit_tests::a_second_sink_is_refused_while_the_first_lives ... verbatim text, already terminated
one line, terminator added by the sink
okreached through the trait
test unit_tests::writing_through_both_methods_reports_nothing_and_panics_on_nothing ... ok
test unit_tests::the_sink_is_the_output_trait_implementation_dirnav_binds_to ... ok
test unit_tests::dropping_a_sink_releases_the_right_to_make_one ... ok
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
=== unit suite rust_textfinder_output returned 0
Four lines in that capture are not the harness speaking. verbatim text, already
terminated, one line, terminator added by the sink, the empty line after
it, and reached through the trait are what two of the tests wrote to the real
stdout, which is the one thing this library owns and therefore the one thing its suite
cannot redirect. The empty line is the output("") case, and seeing it in the
capture is the whole assertion made visible: an empty string still terminates a line.
Four tests for an 83-line library is the right ratio when three of the library's four
properties can only be observed from outside the process. The C++ Output suite asserts nine
things because it can redirect std::cout's stream buffer and inspect the bytes;
this one asserts what holds in process and leaves the bytes to Section 5.
5. Integration Suite Output
Captured in the same run, from run_integration_tests.bat. Each of these 22
tests spawns the executable one or more times, which is why this suite accounts for nearly
all the runtime while the three in-process suites finish in hundredths of a second combined.
The twenty-second arrived with Spec_TextFinder.md §3.6 and asserts an absence:
a_run_that_traverses_nothing_writes_no_summary drives the bare command line,
/H, and a malformed /r and requires that none of the three write
the closing line. A rule about runs that do not traverse can only be tested that way, and
the other 21 tests gained the line in their expected stdout instead - which is what
asserting exact output costs when the output grows, and what it buys.
=== starting integration suite: integration
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.03s
Running tests\integration.rs (target\debug\deps\integration-be2acb173234ccd1.exe)
running 22 tests
test a_malformed_expression_prints_the_listing_to_stdout_then_the_diagnostic ... ok
test a_malformed_expression_under_verbose_lists_the_options_once ... ok
test help_prints_the_specified_text_to_stdout_and_exits_zero ... ok
test a_root_of_dot_contributes_no_leading_dot_slash ... ok
test a_named_root_is_part_of_every_path_and_separators_are_normalized ... ok
test a_bare_command_line_lists_the_resolved_options_and_exits_zero ... ok
test a_block_carries_its_path_once_and_its_detail_lines_beneath_it ... ok
test a_crlf_file_yields_lines_free_of_the_carriage_return ... ok
test an_unopenable_root_is_announced_and_leaves_the_exit_code_zero ... ok
test help_is_taken_before_any_traversal ... ok
test no_path_is_ever_printed_twice ... ok
test recursion_off_searches_the_root_directory_alone ... ok
test the_default_h_hides_only_the_files_that_matched_nothing ... ok
test every_examined_file_appears_exactly_once_under_h_false ... ok
test the_extension_filter_selects_by_last_dot_suffix ... ok
test the_compiled_skip_list_prunes_a_matching_directory ... ok
test the_no_content_case_reports_a_binary_file_and_omits_an_empty_one ... ok
test verbose_lists_the_options_ahead_of_the_search_output ... ok
test each_root_is_traversed_in_the_order_given ... ok
test stdout_is_terminated_with_lf_on_every_platform ... ok
test a_run_that_traverses_nothing_writes_no_summary ... ok
test every_parse_failure_reaches_stderr_with_exit_code_one ... ok
test result: ok. 22 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.84s
=== integration suite integration returned 0
Two tests carry the seven usage diagnostics.
every_parse_failure_reaches_stderr_with_exit_code_one checks the six
parse produces three ways each - the diagnostic text, the exit code, and that
stdout stayed empty - through one expect_usage_failure helper. The third of
those three is the one a reader might not expect, and it is where the specification is
strictest.
a_malformed_expression_prints_the_listing_to_stdout_then_the_diagnostic is the
seventh, and it is the one refusal that leaves the option listing on stdout first.
Four tests cover ordering and terminator rules that no unit suite can reach: that the
/v listing precedes the search output, that help is taken before any traversal,
that stdout carries no CR on any platform, and that a CRLF file yields lines free of the
carriage return. The last of those is the pair to the third: one asserts the program does
not add a CR, the other that it removes one the file carried.
The suite compares expected stdout against help_text() and
usage_line() imported from rust_textfinder_cmdline rather than
against string literals of its own, so the fixture and the source cannot drift. It then
checks the two properties a literal would have caught anyway - that the help text begins
with usage: rust_textfinder [ and ends with searching.\n - so a
library that returned the wrong text consistently still fails.
6. What the Suites Do Not Reach
Every suite passes, and four claims in the specifications are unchecked. Naming them is
worth more than the count above, since §6.2 asks the suites to check a claim rather
than let it be read.
- The unrenderable name. §3.4 settles what happens to a file whose
name the implementation cannot render as text, and Spec_Rust_TextFinder_Dirnav.md
§5 rule 6 places the gate. No fixture creates such a name: it means an unpaired
surrogate on Windows or a non-UTF-8 byte sequence on POSIX, and neither
TempTree builds one. The
C++ suite reports the same
gap, so the rule that cost two implementations a code change is tested in
neither.
- The failed write. Nothing breaks stdout mid-run, so the
output failed notice, the permanence of the failed state, and the rule that
one notice is written rather than one per discarded line are all read rather than
checked.
- Exit code 2. Both of its paths are unreachable from a test. The unit
suite confirms that a second
StdoutSink::new returns None, but
no run of the binary can provoke it, since the binary builds one sink. An undecodable
argument cannot be produced on Windows at all, where every argument arrives as
UTF-16.
- The POSIX side of everything. The capture above is a Windows run.
same_name folds case under #[cfg(windows)] and compares
exactly otherwise, and only the first branch is exercised here.
Three of the four are properties of failure paths, which is the ordinary shape of a coverage
gap. The fourth is a platform, and it is the one a second machine would close without a
line of new test code.
7. Prompt Records
This page carries none. Page_Structure.md §8 assigns it a
Prompts_Rust_Spec_driven_TextFinder_Tests.md, and the turn that wrote the four
suites and the three runners produced no record. The
Process page says what
follows from that for the thread as a whole.