diff --git a/Cargo.lock b/Cargo.lock index 00f5ccd2..a8bad744 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -158,6 +158,17 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "console" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" +dependencies = [ + "encode_unicode", + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -201,6 +212,12 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + [[package]] name = "equivalent" version = "1.0.2" @@ -339,6 +356,18 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "insta" +version = "1.47.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4a6248eb93a4401ed2f37dfe8ea592d3cf05b7cf4f8efa867b6895af7e094e" +dependencies = [ + "console", + "once_cell", + "similar", + "tempfile", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -413,6 +442,7 @@ dependencies = [ "assert_cmd", "clap", "html5ever", + "insta", "libc", "markup5ever_rcdom", "once_cell", @@ -768,6 +798,12 @@ dependencies = [ "syn", ] +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" + [[package]] name = "siphasher" version = "1.0.1" @@ -946,6 +982,12 @@ dependencies = [ "wit-bindgen-rt", ] +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-sys" version = "0.59.0" @@ -964,6 +1006,15 @@ dependencies = [ "windows-targets 0.53.2", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-targets" version = "0.52.6" diff --git a/Cargo.toml b/Cargo.toml index 89255a92..aa5a3164 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ unicode-width = "0.1" [dev-dependencies] rstest = "0.26" assert_cmd = "2" +insta = "1.47" tempfile = "3" libc = "0.2.174" predicates = "3" diff --git a/docs/developers-guide.md b/docs/developers-guide.md index 1c939aa8..e189ea70 100644 --- a/docs/developers-guide.md +++ b/docs/developers-guide.md @@ -156,3 +156,55 @@ Table: Key types and functions. Refer to `docs/adrs/0002-textwrap-wrapping-engine.md` for the rationale behind replacing `LineBuffer` with `textwrap`. + +## CLI matrix harness + +The CLI matrix harness in [tests/cli_matrix.rs](../tests/cli_matrix.rs) checks +that important option combinations keep working through the real `mdtablefix` +binary. It uses `assert_cmd` to run the command and `insta` to snapshot a +labelled envelope containing the case identifier, execution mode, arguments, +exit status, stdout, stderr, and rewritten file content where relevant. + +The base catalogue lives in +[tests/cli_matrix/support.rs](../tests/cli_matrix/support.rs). It covers the +seven non-wrap transform flags: + +- `--renumber` +- `--breaks` +- `--ellipsis` +- `--fences` +- `--footnotes` +- `--code-emphasis` +- `--headings` + +The harness expands every base row into both `--wrap` and no-`--wrap` variants. +It then runs each logical variant twice: once as file-to-stdout formatting and +once with `--in-place` against an equivalent temporary file. The snapshot test +also asserts that stdout output and the `--in-place` rewritten file are +identical for the same logical case. + +Matrix input fixtures live under `tests/data/cli-matrix/` and must use the +`.dat` extension. Do not use `.md` or `.txt` for these fixtures because +`make fmt` runs Markdown formatting and must not rewrite matrix inputs. The +harness has a self-test that rejects non-`.dat` fixtures. + +Before changing snapshots, run the harness self-tests: + +```bash +cargo test --test cli_matrix matrix_case_ids_are_unique +cargo test --test cli_matrix matrix_cases_expand_to_stdout_and_in_place +cargo test --test cli_matrix matrix_cases_expand_to_wrapped_and_unwrapped +cargo test --test cli_matrix matrix_cases_cover_all_transform_pairs +``` + +Create or update snapshots only when the behaviour change is intentional: + +```bash +INSTA_UPDATE=always cargo test --test cli_matrix cli_matrix_snapshots +cargo test --test cli_matrix +``` + +Review the generated `tests/snapshots/cli_matrix__*.snap` files before +committing. Snapshot churn across many cases usually means the fixture is too +broad or a shared transform changed behaviour; inspect the labelled case, mode, +and arguments before accepting the new output. diff --git a/docs/execplans/cli-matrix-testing.md b/docs/execplans/cli-matrix-testing.md new file mode 100644 index 00000000..5e72cec9 --- /dev/null +++ b/docs/execplans/cli-matrix-testing.md @@ -0,0 +1,507 @@ +# Add a CLI option matrix test harness + +This ExecPlan (execution plan) is a living document. The sections `Constraints`, +`Tolerances`, `Risks`, `Progress`, `Surprises & Discoveries`, `Decision Log`, +and `Outcomes & Retrospective` must be kept up to date as work proceeds. + +Status: COMPLETE + +## Purpose / big picture + +After this change, `mdtablefix` will have an integration test harness that +checks important combinations of command-line options together instead of only +checking most flags in isolation. A maintainer will be able to add a new CLI +flag, fixture, or interaction case in one place and rely on harness tests to +prove that the case catalogue still covers the intended option combinations. + +Observable success means `cargo test --test cli_matrix` runs the matrix through +the real `mdtablefix` binary with `assert_cmd`, snapshots each case with +`insta`, and fails clearly when a fixture is missing, a case identifier is +duplicated, a non-`.dat` fixture is used, or the selected case set no longer +covers the required option pairs. Each logical option combination is exercised +twice against the same fixture: once by writing formatted output to stdout and +once with the repository's existing `--in-place` flag. The harness also treats +`--wrap` as required to be paired because earlier `--wrap` and non-`--wrap` +runs have regressed independently. The developer guide will explain how the +harness works, why fixtures use `.dat`, and how to update snapshots +intentionally. + +This ExecPlan is complete and approved. The CLI matrix harness is implemented, +and `docs/developers-guide.md` documents how maintainers use and extend it. + +## Constraints + +- Keep public CLI flags and the public library API stable. +- Exercise the real binary using `assert_cmd`; do not replace the CLI matrix + with direct calls into `process_stream_inner`. +- Use `insta` snapshots for matrix outputs so large expected results do not + become hard-to-review string literals. +- Keep matrix input fixtures in `.dat` files under `tests/data/cli-matrix/` so + `make fmt` and Markdown formatters do not rewrite them. +- For every logical transform combination in the matrix, run both stdout + formatting and `--in-place` formatting against equivalent temporary-file + input. Treat stdout formatting as the ordinary file-to-stdout path, not as a + substitute for exercising `--in-place`. +- For every selected combination of non-wrap transform flags, run both the + `--wrap` and no-`--wrap` variants. This is mandatory regression coverage, not + an optional pairwise side effect. +- Add tests for the matrix harness itself. The harness must not rely on review + discipline alone to preserve fixture naming, case uniqueness, or + combinatorial coverage. +- Do not grow `tests/cli.rs`. It is already 392 lines and the repository file + limit is 400 lines for code files. +- Keep every new Rust source file below 400 lines. Split helpers into a second + test support module if the matrix file approaches that limit. +- Keep CLI output on stdout and diagnostics on stderr. The snapshots must make + those channels distinct. +- Use deterministic case ordering. No randomization, fuzzing seed, or + time-dependent case generation is allowed in this harness. +- Update `docs/developers-guide.md` during implementation so future + maintainers know how to use and extend the matrix harness. + +## Tolerances (exception triggers) + +- Scope: if implementation needs changes to more than 8 files or roughly 450 + net lines of Rust code, stop and re-evaluate the harness shape. +- Dependencies: if adding `insta` requires a Rust version newer than the + repository's `rust-version = "1.89"`, stop and choose a compatible version or + ask for direction. +- Interfaces: if the matrix requires changing any CLI flag name, CLI exit-code + rule, or public library function signature, stop and escalate. +- Coverage: if pairwise coverage across the seven non-wrap transform flags + cannot be achieved in 24 or fewer base rows before `--wrap` and + execution-mode expansion, stop and document why a larger matrix is justified. +- Expansion: if the generated matrix exceeds 96 physical command runs after + expanding each base row into `--wrap` and no-`--wrap` variants and then into + stdout and `--in-place` modes, stop and re-evaluate the base row count. +- Snapshot churn: if one fixture change rewrites more than 30 snapshots, stop + and consider whether the fixture is too broad or the matrix should be split. +- Validation: if `make lint` or `make test` still fails after three focused fix + cycles, stop and capture the failing command, log path, and likely cause in + `Decision Log`. + +## Risks + +- Risk: a full Cartesian product of the eight transform flags, wrapped and + unwrapped variants, and paired stdout and `--in-place` execution would create + hundreds of cases and make snapshot review noisy. Severity: high. Likelihood: + high. Mitigation: use a curated base matrix with generated `--wrap` and + execution-mode expansion, plus harness tests that prove every required option + pair and expansion rule remains covered. + +- Risk: a single overpacked fixture could make failures hard to diagnose. + Severity: medium. Likelihood: medium. Mitigation: use a small set of named + `.dat` fixtures, each aimed at a cluster of behaviours such as tables and + prose, fences and ellipses, frontmatter and breaks, or footnotes and wrapping. + +- Risk: `insta` snapshots can accidentally bless behavioural regressions if + accepted mechanically. Severity: medium. Likelihood: medium. Mitigation: + document the update workflow in `docs/developers-guide.md`, keep snapshot + names stable and descriptive, and snapshot stdout, stderr, exit status, and + in-place file content in a labelled envelope. + +- Risk: `--in-place` has different command-line validity rules from stdin + mode because Clap requires a file path when `--in-place` is set. Severity: + medium. Likelihood: high. Mitigation: model execution mode explicitly in the + harness and add harness tests that reject invalid `--in-place` cases. + +- Risk: the harness might accidentally validate stdout mode and assume + `--in-place` behaves the same, missing file rewrite regressions. Severity: + high. Likelihood: medium. Mitigation: generate stdout and `--in-place` + physical runs from every logical combination and add a harness test proving + that the expansion is complete. + +- Risk: pairwise coverage could technically include `--wrap` without ensuring + every important non-wrap combination is tested both with and without + wrapping. Severity: high. Likelihood: medium. Mitigation: keep `--wrap` out + of the base combination generator, expand each base row into a wrapped and an + unwrapped variant, and add a harness test that proves every non-wrap + signature has both variants. + +- Risk: `make fmt` runs Markdown formatting and could modify Markdown fixtures + if they are stored as `.md` or `.txt` files. Severity: medium. Likelihood: + high. Mitigation: store matrix fixtures with a `.dat` extension and add a + harness test that rejects any case fixture whose path does not end in `.dat`. + +## Progress + +- [x] (2026-04-25 14:19Z) Verified the working tree is on branch + `cli-matrix-testing` and is clean before drafting the plan. +- [x] (2026-04-25 14:19Z) Loaded the `leta`, `execplans`, + `rust-router`, `domain-cli-and-daemons`, and `commit-message` skills needed + for planning, Rust CLI orientation, and the eventual plan commit. +- [x] (2026-04-25 14:19Z) Reviewed `src/main.rs`, `tests/cli.rs`, + `tests/common/mod.rs`, `Cargo.toml`, `Makefile`, and + `docs/developers-guide.md`. +- [x] (2026-04-25 14:19Z) Confirmed the CLI transform switches are `--wrap`, + `--renumber`, `--breaks`, `--ellipsis`, `--fences`, `--footnotes`, + `--code-emphasis`, and `--headings`, with `--in-place` as an execution mode + flag that requires file input. +- [x] (2026-04-25 14:34Z) Updated the draft plan so every logical combination + runs in both stdout and `--in-place` modes and every non-wrap flag + combination runs both with and without `--wrap`. +- [x] (2026-04-26 15:30Z) Received approval to implement the harness, + fixtures, snapshots, and developer-guide documentation. +- [x] (2026-04-26 15:35Z) Added the `insta` dev dependency and `.dat` matrix + fixtures. +- [x] (2026-04-26 15:36Z) Implemented `tests/cli_matrix.rs` with harness + self-tests and snapshot execution. +- [x] (2026-04-26 15:38Z) Generated 32 initial snapshots and verified + `cargo test --test cli_matrix` passes without `INSTA_UPDATE`. +- [x] (2026-04-26 15:43Z) Documented the harness in + `docs/developers-guide.md`. +- [x] (2026-04-26 15:50Z) Ran full validation gates and recorded results. + +## Surprises & discoveries + +- Observation: `grepai` returned no useful semantic hits for this repository, + even after using the `get-project` value `mdtablefix`. Impact: exploration + used `leta` for symbols and targeted exact searches for test dependencies, + CLI flags, and fixture references. + +- Observation: `tests/cli.rs` is 392 lines long. Impact: the implementation + must use a new integration test target such as `tests/cli_matrix.rs` rather + than extending the existing CLI test file. + +- Observation: `assert_cmd` is already present in `Cargo.toml`, but `insta` is + not. Impact: implementation will need a small dev-dependency addition and a + `Cargo.lock` update. + +- Observation: `make fmt` runs both `cargo fmt --all` and `mdformat-all`. + Impact: input fixtures must not use Markdown file extensions, and the harness + should enforce the `.dat` fixture convention directly. + +- Observation: the repository flag is named `--in-place`; the requested + `--inplace` behaviour maps to that existing flag rather than a new CLI + spelling. Impact: the plan uses `--in-place` consistently and does not add an + alias. + +- Observation: previous regressions have appeared only when comparing wrapped + and unwrapped executions. Impact: the matrix must not rely on incidental + pairwise coverage for `--wrap`; it must generate explicit wrapped and + unwrapped variants for each selected non-wrap combination. + +- Observation: the first full `cargo test --test cli_matrix` run failed only + because no `insta` snapshots existed yet, after the harness self-tests + passed. Impact: proceed with the planned `INSTA_UPDATE=always` snapshot + creation step and then rerun the target without `INSTA_UPDATE`. + +- Observation: the first `tests/cli_matrix.rs` draft was 418 lines, exceeding + the repository's 400-line source-file limit. Impact: split reusable matrix + types, fixtures, and command-running helpers into + `tests/cli_matrix/support.rs`, leaving `tests/cli_matrix.rs` focused on the + test assertions. + +- Observation: the first `make lint` run failed on + `clippy::excessive_nesting` in `src/wrap/inline/postprocess.rs`, outside the + new matrix harness. Impact: refactor that existing nested block into a small + helper so the required warning-deny lint gate can pass without suppressing + the lint. + +- Observation: `make fmt` completed successfully but formatted existing + Markdown files outside the scope of this change. Impact: restore unrelated + formatter churn and keep the commit focused on the matrix harness, + developer-guide documentation, ExecPlan updates, dependency lockfile changes, + and the required lint refactor. + +## Decision log + +- Decision: use a curated pairwise matrix rather than a full Cartesian product. + Rationale: the purpose is to catch interactions while keeping snapshot review + tractable. Harness tests will make the selected cases auditable by verifying + pair coverage. Date/Author: 2026-04-25 / Droid. + +- Decision: create a new integration test target, `tests/cli_matrix.rs`, + instead of modifying `tests/cli.rs`. Rationale: `tests/cli.rs` is already + close to the 400-line limit, and the matrix harness is conceptually distinct + from the existing focused CLI regressions. Date/Author: 2026-04-25 / Droid. + +- Decision: snapshot a labelled result envelope rather than only stdout. + Rationale: the CLI contract includes status, stdout, stderr, and, for + `--in-place`, rewritten file content. Keeping those fields labelled avoids + confusing data output with diagnostics. Date/Author: 2026-04-25 / Droid. + +- Decision: keep fixtures in `tests/data/cli-matrix/*.dat`. Rationale: this + satisfies the user requirement that matrix fixtures are not formatted by + `make fmt`, while keeping test data near the existing integration fixtures. + Date/Author: 2026-04-25 / Droid. + +- Decision: model the catalogue as base combinations over the seven non-wrap + transform flags, then expand each base combination into both `--wrap` states + and both file execution modes. Rationale: this guarantees the historically + sensitive wrapped/unwrapped comparison and ensures every combination is + tested through both stdout formatting and `--in-place`. Date/Author: + 2026-04-25 / Droid. + +- Decision: use eight base rows named `row_000` through `row_111`, with each + bit pattern mapped across the seven non-wrap flags, then generate 32 physical + command runs after `--wrap` and execution-mode expansion. Rationale: this + gives every pair of non-wrap flags all four enabled/disabled combinations + while staying well below the 96-run expansion tolerance. Date/Author: + 2026-04-26 / Droid. + +- Decision: split the matrix test support into `tests/cli_matrix/support.rs`. + Rationale: the first implementation of `tests/cli_matrix.rs` exceeded the + repository's 400-line file limit; moving reusable case definitions and + runners into a focused support module keeps both files under the limit. + Date/Author: 2026-04-26 / Droid. + +## Outcomes & retrospective + +The planned harness is implemented. `Cargo.toml` and `Cargo.lock` now include +the `insta` dev dependency. `tests/data/cli-matrix/` contains four `.dat` +fixtures, and `tests/cli_matrix.rs` plus `tests/cli_matrix/support.rs` define +the base catalogue, expansion checks, command runner, and `insta` snapshots. + +The harness now verifies case identifier uniqueness, `.dat` fixture use, +stdout and `--in-place` expansion, wrapped and unwrapped expansion, per-flag +enabled/disabled coverage, and pairwise coverage for the seven non-wrap +transform flags. The snapshot test executes 32 physical command runs and +asserts that file-to-stdout output matches the corresponding `--in-place` +rewritten file. + +`docs/developers-guide.md` now documents the harness purpose, fixture +convention, expansion model, self-tests, and snapshot update workflow. + +Validation passed with these commands and logs: + +```plaintext +cargo test --test cli_matrix matrix_case_ids_are_unique | tee /tmp/cargo-test-cli-matrix-ids-mdtablefix-cli-matrix-testing.out +cargo test --test cli_matrix matrix_cases_expand_to_stdout_and_in_place | tee /tmp/cargo-test-cli-matrix-modes-mdtablefix-cli-matrix-testing.out +cargo test --test cli_matrix matrix_cases_expand_to_wrapped_and_unwrapped | tee /tmp/cargo-test-cli-matrix-wraps-mdtablefix-cli-matrix-testing.out +cargo test --test cli_matrix matrix_cases_cover_all_transform_pairs | tee /tmp/cargo-test-cli-matrix-pairs-mdtablefix-cli-matrix-testing.out +INSTA_UPDATE=always cargo test --test cli_matrix cli_matrix_snapshots | tee /tmp/cargo-test-cli-matrix-snapshot-update-mdtablefix-cli-matrix-testing.out +cargo test --test cli_matrix | tee /tmp/cargo-test-cli-matrix-mdtablefix-cli-matrix-testing.out +make fmt | tee /tmp/fmt-mdtablefix-cli-matrix-testing.out +make check-fmt | tee /tmp/check-fmt-mdtablefix-cli-matrix-testing.out +make lint | tee /tmp/lint-mdtablefix-cli-matrix-testing.out +make test | tee /tmp/test-mdtablefix-cli-matrix-testing.out +make markdownlint | tee /tmp/markdownlint-mdtablefix-cli-matrix-testing.out +make nixie | tee /tmp/nixie-mdtablefix-cli-matrix-testing.out +git diff --check | tee /tmp/diff-check-mdtablefix-cli-matrix-testing.out +``` + +The only implementation surprise was the existing `clippy::excessive_nesting` +finding in `src/wrap/inline/postprocess.rs`; extracting +`carry_previous_inline_code_tail` resolved it without suppressing the lint. + +## Context and orientation + +The CLI entry point is `src/main.rs`. `Cli` defines `--in-place` and delegates +the formatting switches to `FormatOpts`. `FormatOpts` currently exposes eight +independent transform switches: + +- `--wrap` +- `--renumber` +- `--breaks` +- `--ellipsis` +- `--fences` +- `--footnotes` +- `--code-emphasis` +- `--headings` + +`process_lines` in `src/main.rs` first protects leading YAML frontmatter, then +passes the body to `process_stream_inner`. The `--renumber` and `--breaks` +transforms run afterwards in the CLI layer. This means the matrix must include +cases where CLI-only transforms interact with transforms handled inside the +library pipeline. + +The matrix should treat `--wrap` specially. The base catalogue covers the seven +non-wrap transform flags: `--renumber`, `--breaks`, `--ellipsis`, `--fences`, +`--footnotes`, `--code-emphasis`, and `--headings`. The harness then generates +two logical variants from every base row, one without `--wrap` and one with +`--wrap`. It then generates two physical command executions from each logical +variant: ordinary file-to-stdout formatting and `--in-place` formatting against +an equivalent temporary file. + +Current integration helpers live in `tests/common/mod.rs` and are re-exported +through `tests/prelude/mod.rs`. They already provide `run_cli_with_args` and +`run_cli_with_stdin`, both built on `assert_cmd`. The new matrix can reuse the +same import pattern but should run its paired stdout and `--in-place` +executions through temporary files so both physical modes operate on equivalent +file input. It will need richer output capture than the existing helpers +provide because `insta` snapshots should include all relevant process outputs. + +Existing large fixtures live under `tests/data/`. The matrix should add a +subdirectory, `tests/data/cli-matrix/`, to keep fixture ownership obvious and +to make the `.dat` convention easy to enforce. + +## Plan of work + +Stage A adds dependencies and static fixture loading. Add `insta = "1"` to +`[dev-dependencies]` in `Cargo.toml` and update `Cargo.lock` by running the +normal Cargo test command later in the workflow. Create +`tests/data/cli-matrix/` with a small fixture set using `.dat` extensions. +Start with fixtures for: + +- a combined table and prose document that can exercise `--wrap`, + `--renumber`, `--ellipsis`, `--code-emphasis`, and normal table reflow; +- a fence-heavy document that can exercise `--fences`, `--ellipsis`, and + `--renumber` ordering; +- a footnote-heavy document that can exercise `--footnotes`, `--wrap`, and + list renumbering; +- a frontmatter and thematic-break document that can exercise `--headings`, + `--breaks`, and frontmatter preservation. + +Stage B builds the matrix harness in `tests/cli_matrix.rs`. Define small data +types for non-wrap transform flags, wrap variant, execution mode, fixture path, +base matrix case, logical matrix case, and physical command case. Keep the base +case catalogue as deterministic static data, for example `BASE_MATRIX_CASES`. +Each base case must have a stable, filesystem-safe identifier, a fixture path, +and the non-wrap transform flags to pass to the CLI. The harness must expand +each base case into wrapped and unwrapped logical variants, then expand each +logical variant into file-to-stdout and `--in-place` physical runs. The command +runner must use `assert_cmd::Command::cargo_bin("mdtablefix")`, copy the +fixture into a temporary file for both physical modes, assert success for +success cases, and build a snapshot envelope containing status, stdout, stderr, +and file content when relevant. + +Stage C adds harness self-tests before relying on matrix outputs. Add tests +that verify: + +- every case identifier is unique and contains only lowercase letters, digits, + hyphens, and underscores; +- every fixture path exists and ends in `.dat`; +- every generated physical case has both a stdout run and an `--in-place` run + for the same logical combination; +- every selected non-wrap flag signature has both a wrapped and an unwrapped + logical variant; +- every non-wrap transform flag appears enabled and disabled at least once; +- every pair of non-wrap transform flags appears in all four enabled/disabled + combinations across the base case catalogue. + +Stage D adds the matrix execution test and snapshots. Use +`insta::assert_snapshot!` with stable snapshot names derived from the case +identifier. The snapshot value should be a labelled plaintext envelope such as: + +```plaintext +case: wrap-footnotes-stdout +mode: stdout +args: --wrap --footnotes +status: success + +[stdout] +... + +[stderr] +... + +[file] + +``` + +For an `--in-place` physical run, the same logical case should produce a second +snapshot whose mode is `in-place`, whose stdout and stderr are empty on +success, and whose `[file]` section contains the rewritten temporary file. The +harness should also assert that the stdout run's formatted stdout equals the +`--in-place` run's rewritten file content for the same logical case. + +Run the focused matrix test with `INSTA_UPDATE=always` only when creating or +intentionally updating snapshots. Then rerun without `INSTA_UPDATE` to prove no +`.snap.new` files remain. + +Stage E documents the harness in `docs/developers-guide.md`. Add a concise +section named "CLI matrix harness" that explains the purpose, case catalogue, +fixture location, `.dat` requirement, snapshot update workflow, and the harness +self-tests. Include the exact focused commands for reviewing and updating +snapshots. + +Stage F runs repository validation, reviews the changed code for refactoring +needs, updates this plan with results, and commits the approved implementation +only after gates pass. + +## Concrete steps + +Work from the repository root: + +```bash +pwd +git branch --show-current +``` + +Expected: + +```plaintext +/data/leynos/Projects/mdtablefix.worktrees/cli-matrix-testing +cli-matrix-testing +``` + +Add the dependency and fixture skeleton, then create the harness types and +self-tests in `tests/cli_matrix.rs`. Run the focused harness checks first: + +```bash +cargo test --test cli_matrix matrix_case_ids_are_unique +cargo test --test cli_matrix matrix_cases_expand_to_stdout_and_in_place +cargo test --test cli_matrix matrix_cases_expand_to_wrapped_and_unwrapped +cargo test --test cli_matrix matrix_cases_cover_all_transform_pairs +``` + +Expected result: all focused harness tests pass, proving the catalogue can be +trusted before snapshots are reviewed. + +Create initial snapshots intentionally: + +```bash +INSTA_UPDATE=always cargo test --test cli_matrix cli_matrix_snapshots +``` + +Expected result: the test passes and `tests/snapshots/` contains new +`cli_matrix__*.snap` files. Review the snapshots before accepting the change. +Then prove the accepted snapshot set is stable: + +```bash +cargo test --test cli_matrix +``` + +Expected result: all harness and matrix tests pass without producing any +`.snap.new` files. + +After documentation is updated, run formatting and full gates sequentially with +logs: + +```bash +make fmt | tee /tmp/fmt-mdtablefix-cli-matrix-testing.out +make check-fmt | tee /tmp/check-fmt-mdtablefix-cli-matrix-testing.out +make lint | tee /tmp/lint-mdtablefix-cli-matrix-testing.out +make test | tee /tmp/test-mdtablefix-cli-matrix-testing.out +make markdownlint | tee /tmp/markdownlint-mdtablefix-cli-matrix-testing.out +make nixie | tee /tmp/nixie-mdtablefix-cli-matrix-testing.out +git diff --check | tee /tmp/diff-check-mdtablefix-cli-matrix-testing.out +``` + +Expected result: every command exits successfully. If `make nixie` fails due to +an environmental browser issue rather than a Markdown diagram problem, record +the exact failure in this plan and ask for direction before committing. + +## Acceptance criteria + +- `Cargo.toml` and `Cargo.lock` include the `insta` dev dependency with a + Cargo-compatible caret requirement. +- `tests/data/cli-matrix/` contains only `.dat` matrix input fixtures. +- `tests/cli_matrix.rs` uses `assert_cmd` to run the real `mdtablefix` binary. +- `tests/cli_matrix.rs` uses `insta` to snapshot labelled result envelopes. +- Harness self-tests fail if case identifiers are duplicated, fixture paths are + missing, fixtures do not use `.dat`, stdout or `--in-place` expansion is + incomplete, wrapped or unwrapped expansion is incomplete, or pairwise + transform coverage is lost. +- Every logical matrix combination is executed in both file-to-stdout mode and + `--in-place` mode. +- Every selected non-wrap flag combination is executed both with and without + `--wrap`. +- The base matrix covers every enabled/disabled pair combination for + `--renumber`, `--breaks`, `--ellipsis`, `--fences`, `--footnotes`, + `--code-emphasis`, and `--headings`; generated variants then add the required + `--wrap` coverage. +- `docs/developers-guide.md` documents the harness and snapshot update + workflow. +- `make fmt`, `make check-fmt`, `make lint`, `make test`, + `make markdownlint`, `make nixie`, and `git diff --check` pass, with tee logs + recorded under `/tmp`. + +## Rollback plan + +If the implementation creates noisy or unstable snapshots, revert the harness +commit and keep this ExecPlan as the record of the failed approach. If only a +specific fixture or case set is noisy, remove that fixture, update +`MATRIX_CASES`, rerun the harness self-tests, and record the narrower case set +in `Decision Log` before proceeding. diff --git a/src/wrap/inline/postprocess.rs b/src/wrap/inline/postprocess.rs index 7bfc5e24..acfbece3 100644 --- a/src/wrap/inline/postprocess.rs +++ b/src/wrap/inline/postprocess.rs @@ -31,6 +31,35 @@ fn line_has_rebalanceable_tail(line: &[InlineFragment]) -> bool { || (line.len() > 1 && line.last().is_some_and(InlineFragment::is_plain)) } +/// Moves a previous inline-code tail into pending whitespace handling. +fn carry_previous_inline_code_tail( + merged: &mut Vec>, + pending_whitespace: &mut Vec, +) -> bool { + let Some(previous_line) = merged.last_mut() else { + return false; + }; + if !previous_line + .last() + .is_some_and(|fragment| fragment.kind == FragmentKind::InlineCode) + { + return false; + } + + let Some(previous_atomic) = previous_line.pop() else { + debug_assert!( + false, + "inline code tail vanished after successful tail-kind check" + ); + return false; + }; + pending_whitespace.push(previous_atomic); + if previous_line.is_empty() { + merged.pop(); + } + true +} + /// Merges whitespace-only wrap artefacts into neighbouring content lines. /// /// `lines` is the provisional fragment layout from `wrap_first_fit`, and the @@ -56,18 +85,8 @@ pub(super) fn merge_whitespace_only_lines( if line_is_single_space && !next_starts_atomic - && let Some(previous_line) = merged.last_mut() - && previous_line - .last() - .is_some_and(|fragment| fragment.kind == FragmentKind::InlineCode) + && carry_previous_inline_code_tail(&mut merged, &mut pending_whitespace) { - let Some(previous_atomic) = previous_line.pop() else { - continue; - }; - pending_whitespace.push(previous_atomic); - if previous_line.is_empty() { - merged.pop(); - } should_carry_whitespace = true; } diff --git a/tests/cli_matrix.rs b/tests/cli_matrix.rs new file mode 100644 index 00000000..14dc6e68 --- /dev/null +++ b/tests/cli_matrix.rs @@ -0,0 +1,174 @@ +//! Matrix tests for CLI option interactions. + +use std::collections::{BTreeMap, BTreeSet}; + +#[path = "cli_matrix/support.rs"] +mod support; + +use support::{ + ALL_FLAGS, + BASE_MATRIX_CASES, + ExecutionMode, + PhysicalCase, + WrapVariant, + assert_transform_invariants, + fixture_path, + has_flag, + is_case_id, + logical_cases, + non_wrap_signature, + physical_cases, + run_physical_case, +}; + +#[test] +fn matrix_case_ids_are_unique() { + let mut ids = BTreeSet::new(); + for case in BASE_MATRIX_CASES { + assert!(is_case_id(case.id), "invalid case id {}", case.id); + assert!(ids.insert(case.id), "duplicate case id {}", case.id); + } +} + +#[test] +fn matrix_case_ids_accept_documented_characters() { + assert!(is_case_id("row-001_alpha2")); +} + +#[test] +fn matrix_case_fixtures_are_dat_files() { + for case in BASE_MATRIX_CASES { + let fixture = fixture_path(case.fixture); + assert!(fixture.exists(), "missing fixture {}", fixture.display()); + assert_eq!( + fixture.extension().and_then(|ext| ext.to_str()), + Some("dat") + ); + } +} + +#[test] +fn matrix_cases_expand_to_stdout_and_in_place() { + let mut modes_by_logical_id: BTreeMap> = BTreeMap::new(); + for case in physical_cases() { + modes_by_logical_id + .entry(case.logical.id) + .or_default() + .insert(case.mode); + } + + for (id, modes) in modes_by_logical_id { + assert_eq!( + modes, + BTreeSet::from([ExecutionMode::Stdout, ExecutionMode::InPlace]), + "logical case {id} must run in both modes", + ); + } +} + +#[test] +fn matrix_cases_expand_to_wrapped_and_unwrapped() { + let mut wraps_by_signature: BTreeMap> = BTreeMap::new(); + for case in logical_cases() { + let variant = if case.is_wrapped { + WrapVariant::Wrapped + } else { + WrapVariant::Unwrapped + }; + wraps_by_signature + .entry(non_wrap_signature(case.fixture, &case.flags)) + .or_default() + .insert(variant); + } + + for (signature, variants) in wraps_by_signature { + assert_eq!( + variants, + BTreeSet::from([WrapVariant::Wrapped, WrapVariant::Unwrapped]), + "non-wrap signature {signature} must have both wrap variants", + ); + } +} + +#[test] +fn matrix_cases_cover_all_transform_pairs() { + for (left_index, left) in ALL_FLAGS.iter().enumerate() { + for right in ALL_FLAGS.iter().skip(left_index + 1) { + let mut combinations = BTreeSet::new(); + for case in BASE_MATRIX_CASES { + combinations.insert((has_flag(case, *left), has_flag(case, *right))); + } + assert_eq!( + combinations, + BTreeSet::from([(false, false), (false, true), (true, false), (true, true)]), + "missing pair coverage for {} and {}", + left.as_arg(), + right.as_arg(), + ); + } + } +} + +#[test] +fn matrix_cases_enable_and_disable_each_transform() { + for flag in ALL_FLAGS { + let mut states = BTreeSet::new(); + for case in BASE_MATRIX_CASES { + states.insert(has_flag(case, *flag)); + } + assert_eq!( + states, + BTreeSet::from([false, true]), + "{} must appear enabled and disabled", + flag.as_arg(), + ); + } +} + +#[test] +fn cli_matrix_snapshots() -> anyhow::Result<()> { + for logical in logical_cases() { + let stdout_case = PhysicalCase { + logical: logical.clone(), + mode: ExecutionMode::Stdout, + }; + let in_place_case = PhysicalCase { + logical, + mode: ExecutionMode::InPlace, + }; + + let stdout_result = run_physical_case(&stdout_case).expect("run physical case"); + assert!( + stdout_result.output.status.success(), + "{} failed with stderr:\n{}", + stdout_case.snapshot_name(), + String::from_utf8_lossy(&stdout_result.output.stderr), + ); + assert_transform_invariants(&stdout_case.logical, &stdout_result.output.stdout)?; + + let in_place_result = run_physical_case(&in_place_case).expect("run physical case"); + assert!( + in_place_result.output.status.success(), + "{} failed with stderr:\n{}", + in_place_case.snapshot_name(), + String::from_utf8_lossy(&in_place_result.output.stderr), + ); + assert_transform_invariants(&in_place_case.logical, &in_place_result.file_content)?; + + assert_eq!( + stdout_result.output.stdout, in_place_result.file_content, + "{} stdout must match in-place file output", + stdout_case.logical.id, + ); + + insta::assert_snapshot!( + stdout_case.snapshot_name(), + stdout_result.envelope(&stdout_case) + ); + insta::assert_snapshot!( + in_place_case.snapshot_name(), + in_place_result.envelope(&in_place_case), + ); + } + Ok(()) +} diff --git a/tests/cli_matrix/invariants.rs b/tests/cli_matrix/invariants.rs new file mode 100644 index 00000000..61d22e09 --- /dev/null +++ b/tests/cli_matrix/invariants.rs @@ -0,0 +1,176 @@ +//! Independent output invariants for CLI matrix snapshots. + +use std::fs; + +use anyhow::{Context as _, Result}; + +use super::{LogicalCase, TransformFlag, fixture_path}; + +const ELLIPSIS_UTF8: &[u8] = b"\xE2\x80\xA6"; + +struct OrderedMarker { + indent: String, + number: usize, +} + +/// Asserts output properties that prove enabled transforms changed matching input. +pub(crate) fn assert_transform_invariants(logical: &LogicalCase, stdout: &[u8]) -> Result<()> { + let fixture_path = fixture_path(logical.fixture); + let fixture = fs::read_to_string(&fixture_path) + .with_context(|| format!("read matrix fixture '{}'", fixture_path.display()))?; + let output = String::from_utf8_lossy(stdout); + + if logical.flags.contains(&TransformFlag::Ellipsis) && fixture.contains("...") { + // Validates `--ellipsis`: textual dots in the fixture must become a Unicode ellipsis. + assert!( + stdout + .windows(ELLIPSIS_UTF8.len()) + .any(|window| window == ELLIPSIS_UTF8), + "{} should contain a UTF-8 Unicode ellipsis", + logical.id, + ); + } + if logical.flags.contains(&TransformFlag::Fences) && fixture_has_fence_candidate(&fixture) { + // Validates `--fences`: eligible fence input must be normalised to backtick fences. + assert!( + output.contains("```"), + "{} should contain a backtick fenced code block delimiter", + logical.id, + ); + } + if logical.flags.contains(&TransformFlag::Renumber) { + for marker in unordered_fixture_markers(&fixture) { + // Validates `--renumber`: out-of-sequence fixture markers must not survive. + assert!( + !output.lines().any(|line| line.starts_with(&marker)), + "{} should not retain unordered list marker {marker:?}", + logical.id, + ); + } + } + Ok(()) +} + +fn fixture_has_fence_candidate(fixture: &str) -> bool { + fixture.lines().any(|line| { + line.trim_start().starts_with("```") + || line.trim_start().starts_with("~~~") + || line.starts_with(" ") + }) +} + +fn unordered_fixture_markers(fixture: &str) -> Vec { + let mut markers = Vec::new(); + let mut expected_by_indent: std::collections::BTreeMap = + std::collections::BTreeMap::new(); + + for line in fixture.lines() { + if line.trim().is_empty() { + expected_by_indent.clear(); + continue; + } + let Some(marker) = ordered_marker(line) else { + continue; + }; + let indent_len = marker.indent.len(); + // Remove deeper-nested counters when we return to a shallower level. + expected_by_indent.retain(|&k, _| k <= indent_len); + let expected = *expected_by_indent.get(&indent_len).unwrap_or(&1); + if marker.number != expected { + markers.push(format!("{}{}. ", marker.indent, marker.number)); + } + expected_by_indent.insert(indent_len, expected + 1); + } + markers +} + +fn ordered_marker(line: &str) -> Option { + let indent_length = line.len() - line.trim_start().len(); + let (digits, rest) = line[indent_length..].split_once('.')?; + rest.starts_with(' ') + .then_some(digits) + .filter(|digits| !digits.is_empty())? + .parse() + .ok() + .map(|number| OrderedMarker { + indent: line[..indent_length].to_string(), + number, + }) +} + +#[cfg(test)] +mod tests { + use super::{fixture_has_fence_candidate, ordered_marker, unordered_fixture_markers}; + + #[test] + fn ordered_marker_returns_none_for_plain_prose() { + assert!(ordered_marker("plain prose").is_none()); + } + + #[test] + fn ordered_marker_returns_none_without_dot_space_suffix() { + assert!(ordered_marker("123 item").is_none()); + } + + #[test] + fn ordered_marker_returns_indent_and_number() { + let marker = ordered_marker(" 3. item").expect("parse ordered marker"); + + assert_eq!(marker.indent, " "); + assert_eq!(marker.number, 3); + } + + #[test] + fn ordered_marker_returns_none_for_empty_string() { + assert!(ordered_marker("").is_none()); + } + + #[test] + fn fixture_has_fence_candidate_detects_backtick_fence() { + assert!(fixture_has_fence_candidate("```rust\ncode\n```")); + } + + #[test] + fn fixture_has_fence_candidate_detects_tilde_fence() { + assert!(fixture_has_fence_candidate("~~~rust\ncode\n~~~")); + } + + #[test] + fn fixture_has_fence_candidate_detects_indented_code() { + assert!(fixture_has_fence_candidate(" code")); + } + + #[test] + fn fixture_has_fence_candidate_rejects_plain_text() { + assert!(!fixture_has_fence_candidate("plain prose\nmore prose")); + } + + #[test] + fn unordered_fixture_markers_accepts_numbered_flat_list() { + assert!(unordered_fixture_markers("1. a\n2. b\n3. c").is_empty()); + } + + #[test] + fn unordered_fixture_markers_flags_flat_out_of_order_marker() { + assert_eq!(unordered_fixture_markers("1. a\n3. b"), ["3. "]); + } + + #[test] + fn unordered_fixture_markers_resets_counter_after_blank_line() { + assert_eq!(unordered_fixture_markers("1. a\n\n3. b"), ["3. "]); + } + + #[test] + fn unordered_fixture_markers_tracks_nested_counters_independently() { + let fixture = "1. outer\n 1. inner\n 2. inner2\n2. outer2"; + + assert!(unordered_fixture_markers(fixture).is_empty()); + } + + #[test] + fn unordered_fixture_markers_flags_nested_out_of_order_marker() { + let fixture = "1. outer\n 1. inner\n 3. inner2\n2. outer2"; + + assert_eq!(unordered_fixture_markers(fixture), [" 3. "]); + } +} diff --git a/tests/cli_matrix/support.rs b/tests/cli_matrix/support.rs new file mode 100644 index 00000000..8f92b63c --- /dev/null +++ b/tests/cli_matrix/support.rs @@ -0,0 +1,397 @@ +//! Support types and runners for the CLI matrix integration test. + +use std::{ + fs, + path::{Path, PathBuf}, + process::Output, +}; + +use anyhow::{Context as _, Result}; +use assert_cmd::Command; +use tempfile::tempdir; + +#[path = "invariants.rs"] +mod invariants; + +/// Represents a non-wrap CLI transform flag. +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub(crate) enum TransformFlag { + /// Renumbers ordered list items. + Renumber, + /// Reformats thematic breaks. + Breaks, + /// Replaces textual ellipsis sequences. + Ellipsis, + /// Normalizes fenced code block delimiters. + Fences, + /// Converts bare numeric references to footnotes. + Footnotes, + /// Fixes emphasis markers adjacent to inline code. + CodeEmphasis, + /// Converts Setext headings to ATX headings. + Headings, +} + +impl TransformFlag { + /// Returns the command-line argument for this transform flag. + pub(crate) fn as_arg(self) -> &'static str { + match self { + Self::Renumber => "--renumber", + Self::Breaks => "--breaks", + Self::Ellipsis => "--ellipsis", + Self::Fences => "--fences", + Self::Footnotes => "--footnotes", + Self::CodeEmphasis => "--code-emphasis", + Self::Headings => "--headings", + } + } +} + +#[derive(Clone, Copy)] +/// Defines one curated base row before wrap and execution-mode expansion. +pub(crate) struct BaseCase { + /// Stable identifier for the base matrix row. + pub(crate) id: &'static str, + /// Fixture filename under `tests/data/cli-matrix/`. + pub(crate) fixture: &'static str, + /// Non-wrap transform flags enabled for this base row. + pub(crate) flags: &'static [TransformFlag], +} + +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +/// Represents whether `--wrap` is active. +pub(crate) enum WrapVariant { + /// Enables `--wrap` for the logical case. + Wrapped, + /// Leaves `--wrap` disabled for the logical case. + Unwrapped, +} + +impl WrapVariant { + fn id_part(self) -> &'static str { + match self { + Self::Wrapped => "wrap", + Self::Unwrapped => "nowrap", + } + } +} + +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +/// Represents how the binary is invoked. +pub(crate) enum ExecutionMode { + /// Writes formatted output to stdout. + Stdout, + /// Rewrites the temporary input file with `--in-place`. + InPlace, +} + +impl ExecutionMode { + fn id_part(self) -> &'static str { + match self { + Self::Stdout => "stdout", + Self::InPlace => "in_place", + } + } +} + +#[derive(Clone)] +/// Represents one base row after wrap expansion. +pub(crate) struct LogicalCase { + /// Stable logical identifier used in snapshot names. + pub(crate) id: String, + /// Fixture filename under `tests/data/cli-matrix/`. + pub(crate) fixture: &'static str, + /// Whether the logical case includes `--wrap`. + pub(crate) is_wrapped: bool, + /// Non-wrap transform flags enabled for the logical case. + pub(crate) flags: Vec, +} + +/// Represents one executable matrix case after mode expansion. +pub(crate) struct PhysicalCase { + /// Logical case being executed. + pub(crate) logical: LogicalCase, + /// Invocation mode for this physical command run. + pub(crate) mode: ExecutionMode, +} + +/// Captures command output and any rewritten file content. +pub(crate) struct RunResult { + /// Process output returned by the `mdtablefix` binary. + pub(crate) output: Output, + /// Bytes read from the temporary input file after execution. + pub(crate) file_content: Vec, +} + +/// Ordered slice of every non-wrap transform flag. +pub(crate) const ALL_FLAGS: &[TransformFlag] = &[ + TransformFlag::Renumber, + TransformFlag::Breaks, + TransformFlag::Ellipsis, + TransformFlag::Fences, + TransformFlag::Footnotes, + TransformFlag::CodeEmphasis, + TransformFlag::Headings, +]; + +/// Curated pairwise base matrix rows. +pub(crate) const BASE_MATRIX_CASES: &[BaseCase] = &[ + BaseCase { + id: "row_000", + fixture: "table-prose.dat", + flags: &[], + }, + BaseCase { + id: "row_001", + fixture: "fences-ellipsis.dat", + flags: &[ + TransformFlag::Ellipsis, + TransformFlag::Footnotes, + TransformFlag::CodeEmphasis, + TransformFlag::Headings, + ], + }, + BaseCase { + id: "row_010", + fixture: "footnotes.dat", + flags: &[ + TransformFlag::Breaks, + TransformFlag::Fences, + TransformFlag::CodeEmphasis, + TransformFlag::Headings, + ], + }, + BaseCase { + id: "row_011", + fixture: "frontmatter-breaks.dat", + flags: &[ + TransformFlag::Breaks, + TransformFlag::Ellipsis, + TransformFlag::Fences, + TransformFlag::Footnotes, + ], + }, + BaseCase { + id: "row_100", + fixture: "table-prose.dat", + flags: &[ + TransformFlag::Renumber, + TransformFlag::Fences, + TransformFlag::Footnotes, + TransformFlag::Headings, + ], + }, + BaseCase { + id: "row_101", + fixture: "fences-ellipsis.dat", + flags: &[ + TransformFlag::Renumber, + TransformFlag::Ellipsis, + TransformFlag::Fences, + TransformFlag::CodeEmphasis, + ], + }, + BaseCase { + id: "row_110", + fixture: "footnotes.dat", + flags: &[ + TransformFlag::Renumber, + TransformFlag::Breaks, + TransformFlag::Footnotes, + TransformFlag::CodeEmphasis, + ], + }, + BaseCase { + id: "row_111", + fixture: "frontmatter-breaks.dat", + flags: &[ + TransformFlag::Renumber, + TransformFlag::Breaks, + TransformFlag::Ellipsis, + TransformFlag::Headings, + ], + }, +]; + +impl RunResult { + /// Builds the labelled text snapshot for a physical command run. + pub(crate) fn envelope(&self, case: &PhysicalCase) -> String { + let stdout = String::from_utf8_lossy(&self.output.stdout); + let stderr = String::from_utf8_lossy(&self.output.stderr); + let file = if case.mode == ExecutionMode::InPlace { + String::from_utf8_lossy(&self.file_content).into_owned() + } else { + "\n".to_string() + }; + + format!( + "case: {}\nmode: {}\nargs: {}\nstatus: {}\n\n[stdout]\n{}\n[stderr]\n{}\n[file]\n{}", + case.logical.id, + case.mode.id_part(), + case.args().join(" "), + self.output.status, + stdout, + stderr, + file, + ) + } +} + +impl PhysicalCase { + /// Returns the stable snapshot name for this physical command run. + pub(crate) fn snapshot_name(&self) -> String { + format!("{}_{}", self.logical.id, self.mode.id_part()) + } + + /// Builds the CLI argument list for this physical command run. + pub(crate) fn args(&self) -> Vec<&'static str> { + let mut args = Vec::new(); + if self.logical.is_wrapped { + args.push("--wrap"); + } + args.extend(self.logical.flags.iter().map(|flag| flag.as_arg())); + if self.mode == ExecutionMode::InPlace { + args.push("--in-place"); + } + args + } +} + +/// Expands every base row into wrapped and unwrapped logical cases. +pub(crate) fn logical_cases() -> Vec { + BASE_MATRIX_CASES + .iter() + .flat_map(|case| { + [WrapVariant::Unwrapped, WrapVariant::Wrapped].map(move |variant| LogicalCase { + id: format!("{}_{}", case.id, variant.id_part()), + fixture: case.fixture, + is_wrapped: variant == WrapVariant::Wrapped, + flags: case.flags.to_vec(), + }) + }) + .collect() +} + +/// Expands every logical case into stdout and `--in-place` command runs. +pub(crate) fn physical_cases() -> Vec { + logical_cases() + .into_iter() + .flat_map(|logical| { + [ExecutionMode::Stdout, ExecutionMode::InPlace].map(move |mode| PhysicalCase { + logical: logical.clone(), + mode, + }) + }) + .collect() +} + +/// Asserts output properties that prove enabled transforms changed matching input. +pub(crate) fn assert_transform_invariants(logical: &LogicalCase, stdout: &[u8]) -> Result<()> { + invariants::assert_transform_invariants(logical, stdout) +} + +/// Copies a matrix fixture into the temporary command directory. +/// +/// The staged input preserves the fixture extension for debugging clarity. +pub(crate) fn stage_fixture(case: &PhysicalCase, dir: &Path) -> Result { + let fixture = fixture_path(case.logical.fixture); + let file_path = dir + .join("input") + .with_extension(fixture.extension().unwrap_or_default()); + fs::copy(&fixture, &file_path).with_context(|| { + format!( + "copy fixture '{}' to '{}'", + case.logical.fixture, + file_path.display(), + ) + })?; + Ok(file_path) +} + +/// Builds a run result from process output and the temporary input file. +pub(crate) fn collect_result( + output: Output, + file_path: &Path, + mode: ExecutionMode, +) -> Result { + let file_content = match mode { + ExecutionMode::Stdout | ExecutionMode::InPlace => fs::read(file_path) + .with_context(|| format!("read file '{}' after {:?} run", file_path.display(), mode))?, + }; + Ok(RunResult { + output, + file_content, + }) +} + +/// Runs a physical matrix case through the real `mdtablefix` binary. +pub(crate) fn run_physical_case(case: &PhysicalCase) -> Result { + let dir = tempdir().context("create temporary directory for matrix case")?; + let file_path = stage_fixture(case, dir.path())?; + + let mut command = Command::cargo_bin("mdtablefix").context("create mdtablefix test command")?; + command.args(case.args()).arg(&file_path); + let output = command.output().with_context(|| { + format!( + "execute mdtablefix for matrix case '{}'", + case.snapshot_name() + ) + })?; + collect_result(output, &file_path, case.mode) +} + +/// Returns the repository-relative path to a matrix fixture. +pub(crate) fn fixture_path(file_name: &str) -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests") + .join("data") + .join("cli-matrix") + .join(file_name) +} + +/// Returns whether a matrix case identifier uses the documented character set. +pub(crate) fn is_case_id(id: &str) -> bool { + !id.is_empty() + && id.bytes().all(|byte| { + byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_' || byte == b'-' + }) +} + +/// Builds a signature that ignores the wrap variant. +pub(crate) fn non_wrap_signature(fixture: &str, flags: &[TransformFlag]) -> String { + let args = flags + .iter() + .map(|flag| flag.as_arg()) + .collect::>() + .join(","); + format!("{fixture}:{args}") +} + +/// Returns whether a base row enables the given transform flag. +pub(crate) fn has_flag(case: &BaseCase, flag: TransformFlag) -> bool { case.flags.contains(&flag) } + +#[cfg(test)] +#[rustfmt::skip] +mod tests { use super::{BaseCase, TransformFlag, has_flag, is_case_id, non_wrap_signature}; + use rstest::rstest; + + #[rstest] + #[case("row_001", true)] #[case("row-001", true)] #[case("abc123", true)] + #[case("", false)] #[case("Row_001", false)] #[case("row 001", false)] + fn is_case_id_returns_expected_value(#[case] id: &str, #[case] expected: bool) { + assert_eq!(is_case_id(id), expected); + } + + #[test] fn non_wrap_signature_ignores_wrap_variant() { + let flags = [TransformFlag::Renumber, TransformFlag::Fences]; let (unwrapped, wrapped) = (false, true); + assert_ne!(unwrapped, wrapped); assert_eq!(non_wrap_signature("fixture.dat", &flags), non_wrap_signature("fixture.dat", &flags)); } + #[test] fn non_wrap_signature_distinguishes_flag_lists() { + assert_ne!(non_wrap_signature("fixture.dat", &[TransformFlag::Renumber]), non_wrap_signature("fixture.dat", &[TransformFlag::Fences])); } + + #[rstest] + #[case(TransformFlag::Renumber, true)] #[case(TransformFlag::Fences, false)] + fn has_flag_returns_expected_value(#[case] flag: TransformFlag, #[case] expected: bool) { + let case = BaseCase { id: "row_001", fixture: "fixture.dat", flags: &[TransformFlag::Renumber] }; + assert_eq!(has_flag(&case, flag), expected); + } +} diff --git a/tests/data/cli-matrix/fences-ellipsis.dat b/tests/data/cli-matrix/fences-ellipsis.dat new file mode 100644 index 00000000..841d8daf --- /dev/null +++ b/tests/data/cli-matrix/fences-ellipsis.dat @@ -0,0 +1,11 @@ +Rust +~~~~ +fn main() { + println!("dots..."); +} +~~~~ + +Outside... text. + +1. first +4. second diff --git a/tests/data/cli-matrix/footnotes.dat b/tests/data/cli-matrix/footnotes.dat new file mode 100644 index 00000000..a38e4a03 --- /dev/null +++ b/tests/data/cli-matrix/footnotes.dat @@ -0,0 +1,4 @@ +This paragraph references item 1 and uses enough words to demonstrate wrapping behaviour when the wrap variant is enabled. + +1. https://example.com/alpha +3. https://example.com/beta diff --git a/tests/data/cli-matrix/frontmatter-breaks.dat b/tests/data/cli-matrix/frontmatter-breaks.dat new file mode 100644 index 00000000..69961b3b --- /dev/null +++ b/tests/data/cli-matrix/frontmatter-breaks.dat @@ -0,0 +1,10 @@ +--- +title: Matrix +--- + +Heading +----- + +--- + +Text... after break. diff --git a/tests/data/cli-matrix/table-prose.dat b/tests/data/cli-matrix/table-prose.dat new file mode 100644 index 00000000..b2520543 --- /dev/null +++ b/tests/data/cli-matrix/table-prose.dat @@ -0,0 +1,7 @@ +| Name | Notes | |---|---| | alpha | Use *`cargo test`* before merging... | + +1. first item +3. second item with enough text to wrap around the configured column width when wrapping is enabled for the matrix harness. + +Title +===== diff --git a/tests/snapshots/cli_matrix__row_000_nowrap_in_place.snap b/tests/snapshots/cli_matrix__row_000_nowrap_in_place.snap new file mode 100644 index 00000000..b529a1a7 --- /dev/null +++ b/tests/snapshots/cli_matrix__row_000_nowrap_in_place.snap @@ -0,0 +1,23 @@ +--- +source: tests/cli_matrix.rs +expression: in_place_result.envelope(&in_place_case) +--- +case: row_000_nowrap +mode: in_place +args: --in-place +status: exit status: 0 + +[stdout] + +[stderr] + +[file] +| Name | Notes | +| ----- | ------------------------------------ | +| alpha | Use *`cargo test`* before merging... | + +1. first item +3. second item with enough text to wrap around the configured column width when wrapping is enabled for the matrix harness. + +Title +===== diff --git a/tests/snapshots/cli_matrix__row_000_nowrap_stdout.snap b/tests/snapshots/cli_matrix__row_000_nowrap_stdout.snap new file mode 100644 index 00000000..00ca36e8 --- /dev/null +++ b/tests/snapshots/cli_matrix__row_000_nowrap_stdout.snap @@ -0,0 +1,24 @@ +--- +source: tests/cli_matrix.rs +expression: stdout_result.envelope(&stdout_case) +--- +case: row_000_nowrap +mode: stdout +args: +status: exit status: 0 + +[stdout] +| Name | Notes | +| ----- | ------------------------------------ | +| alpha | Use *`cargo test`* before merging... | + +1. first item +3. second item with enough text to wrap around the configured column width when wrapping is enabled for the matrix harness. + +Title +===== + +[stderr] + +[file] + diff --git a/tests/snapshots/cli_matrix__row_000_wrap_in_place.snap b/tests/snapshots/cli_matrix__row_000_wrap_in_place.snap new file mode 100644 index 00000000..2101082f --- /dev/null +++ b/tests/snapshots/cli_matrix__row_000_wrap_in_place.snap @@ -0,0 +1,23 @@ +--- +source: tests/cli_matrix.rs +expression: in_place_result.envelope(&in_place_case) +--- +case: row_000_wrap +mode: in_place +args: --wrap --in-place +status: exit status: 0 + +[stdout] + +[stderr] + +[file] +| Name | Notes | +| ----- | ------------------------------------ | +| alpha | Use *`cargo test`* before merging... | + +1. first item +3. second item with enough text to wrap around the configured column width when + wrapping is enabled for the matrix harness. + +Title ===== diff --git a/tests/snapshots/cli_matrix__row_000_wrap_stdout.snap b/tests/snapshots/cli_matrix__row_000_wrap_stdout.snap new file mode 100644 index 00000000..2ed78e66 --- /dev/null +++ b/tests/snapshots/cli_matrix__row_000_wrap_stdout.snap @@ -0,0 +1,24 @@ +--- +source: tests/cli_matrix.rs +expression: stdout_result.envelope(&stdout_case) +--- +case: row_000_wrap +mode: stdout +args: --wrap +status: exit status: 0 + +[stdout] +| Name | Notes | +| ----- | ------------------------------------ | +| alpha | Use *`cargo test`* before merging... | + +1. first item +3. second item with enough text to wrap around the configured column width when + wrapping is enabled for the matrix harness. + +Title ===== + +[stderr] + +[file] + diff --git a/tests/snapshots/cli_matrix__row_001_nowrap_in_place.snap b/tests/snapshots/cli_matrix__row_001_nowrap_in_place.snap new file mode 100644 index 00000000..b0f91059 --- /dev/null +++ b/tests/snapshots/cli_matrix__row_001_nowrap_in_place.snap @@ -0,0 +1,25 @@ +--- +source: tests/cli_matrix.rs +expression: in_place_result.envelope(&in_place_case) +--- +case: row_001_nowrap +mode: in_place +args: --ellipsis --footnotes --code-emphasis --headings --in-place +status: exit status: 0 + +[stdout] + +[stderr] + +[file] +Rust +~~~~ +fn main() { + println!("dots..."); +} +~~~~ + +Outside… text. + +1. first +4. second diff --git a/tests/snapshots/cli_matrix__row_001_nowrap_stdout.snap b/tests/snapshots/cli_matrix__row_001_nowrap_stdout.snap new file mode 100644 index 00000000..8b055e03 --- /dev/null +++ b/tests/snapshots/cli_matrix__row_001_nowrap_stdout.snap @@ -0,0 +1,26 @@ +--- +source: tests/cli_matrix.rs +expression: stdout_result.envelope(&stdout_case) +--- +case: row_001_nowrap +mode: stdout +args: --ellipsis --footnotes --code-emphasis --headings +status: exit status: 0 + +[stdout] +Rust +~~~~ +fn main() { + println!("dots..."); +} +~~~~ + +Outside… text. + +1. first +4. second + +[stderr] + +[file] + diff --git a/tests/snapshots/cli_matrix__row_001_wrap_in_place.snap b/tests/snapshots/cli_matrix__row_001_wrap_in_place.snap new file mode 100644 index 00000000..9bc2ade6 --- /dev/null +++ b/tests/snapshots/cli_matrix__row_001_wrap_in_place.snap @@ -0,0 +1,25 @@ +--- +source: tests/cli_matrix.rs +expression: in_place_result.envelope(&in_place_case) +--- +case: row_001_wrap +mode: in_place +args: --wrap --ellipsis --footnotes --code-emphasis --headings --in-place +status: exit status: 0 + +[stdout] + +[stderr] + +[file] +Rust +~~~~ +fn main() { + println!("dots..."); +} +~~~~ + +Outside… text. + +1. first +4. second diff --git a/tests/snapshots/cli_matrix__row_001_wrap_stdout.snap b/tests/snapshots/cli_matrix__row_001_wrap_stdout.snap new file mode 100644 index 00000000..9b1083f7 --- /dev/null +++ b/tests/snapshots/cli_matrix__row_001_wrap_stdout.snap @@ -0,0 +1,26 @@ +--- +source: tests/cli_matrix.rs +expression: stdout_result.envelope(&stdout_case) +--- +case: row_001_wrap +mode: stdout +args: --wrap --ellipsis --footnotes --code-emphasis --headings +status: exit status: 0 + +[stdout] +Rust +~~~~ +fn main() { + println!("dots..."); +} +~~~~ + +Outside… text. + +1. first +4. second + +[stderr] + +[file] + diff --git a/tests/snapshots/cli_matrix__row_010_nowrap_in_place.snap b/tests/snapshots/cli_matrix__row_010_nowrap_in_place.snap new file mode 100644 index 00000000..02503a92 --- /dev/null +++ b/tests/snapshots/cli_matrix__row_010_nowrap_in_place.snap @@ -0,0 +1,18 @@ +--- +source: tests/cli_matrix.rs +expression: in_place_result.envelope(&in_place_case) +--- +case: row_010_nowrap +mode: in_place +args: --breaks --fences --code-emphasis --headings --in-place +status: exit status: 0 + +[stdout] + +[stderr] + +[file] +This paragraph references item 1 and uses enough words to demonstrate wrapping behaviour when the wrap variant is enabled. + +1. https://example.com/alpha +3. https://example.com/beta diff --git a/tests/snapshots/cli_matrix__row_010_nowrap_stdout.snap b/tests/snapshots/cli_matrix__row_010_nowrap_stdout.snap new file mode 100644 index 00000000..8de93b93 --- /dev/null +++ b/tests/snapshots/cli_matrix__row_010_nowrap_stdout.snap @@ -0,0 +1,19 @@ +--- +source: tests/cli_matrix.rs +expression: stdout_result.envelope(&stdout_case) +--- +case: row_010_nowrap +mode: stdout +args: --breaks --fences --code-emphasis --headings +status: exit status: 0 + +[stdout] +This paragraph references item 1 and uses enough words to demonstrate wrapping behaviour when the wrap variant is enabled. + +1. https://example.com/alpha +3. https://example.com/beta + +[stderr] + +[file] + diff --git a/tests/snapshots/cli_matrix__row_010_wrap_in_place.snap b/tests/snapshots/cli_matrix__row_010_wrap_in_place.snap new file mode 100644 index 00000000..2e86ea3a --- /dev/null +++ b/tests/snapshots/cli_matrix__row_010_wrap_in_place.snap @@ -0,0 +1,19 @@ +--- +source: tests/cli_matrix.rs +expression: in_place_result.envelope(&in_place_case) +--- +case: row_010_wrap +mode: in_place +args: --wrap --breaks --fences --code-emphasis --headings --in-place +status: exit status: 0 + +[stdout] + +[stderr] + +[file] +This paragraph references item 1 and uses enough words to demonstrate wrapping +behaviour when the wrap variant is enabled. + +1. https://example.com/alpha +3. https://example.com/beta diff --git a/tests/snapshots/cli_matrix__row_010_wrap_stdout.snap b/tests/snapshots/cli_matrix__row_010_wrap_stdout.snap new file mode 100644 index 00000000..6a17b72b --- /dev/null +++ b/tests/snapshots/cli_matrix__row_010_wrap_stdout.snap @@ -0,0 +1,20 @@ +--- +source: tests/cli_matrix.rs +expression: stdout_result.envelope(&stdout_case) +--- +case: row_010_wrap +mode: stdout +args: --wrap --breaks --fences --code-emphasis --headings +status: exit status: 0 + +[stdout] +This paragraph references item 1 and uses enough words to demonstrate wrapping +behaviour when the wrap variant is enabled. + +1. https://example.com/alpha +3. https://example.com/beta + +[stderr] + +[file] + diff --git a/tests/snapshots/cli_matrix__row_011_nowrap_in_place.snap b/tests/snapshots/cli_matrix__row_011_nowrap_in_place.snap new file mode 100644 index 00000000..1c81eb56 --- /dev/null +++ b/tests/snapshots/cli_matrix__row_011_nowrap_in_place.snap @@ -0,0 +1,24 @@ +--- +source: tests/cli_matrix.rs +expression: in_place_result.envelope(&in_place_case) +--- +case: row_011_nowrap +mode: in_place +args: --breaks --ellipsis --fences --footnotes --in-place +status: exit status: 0 + +[stdout] + +[stderr] + +[file] +--- +title: Matrix +--- + +Heading +______________________________________________________________________ + +______________________________________________________________________ + +Text… after break. diff --git a/tests/snapshots/cli_matrix__row_011_nowrap_stdout.snap b/tests/snapshots/cli_matrix__row_011_nowrap_stdout.snap new file mode 100644 index 00000000..20cfffde --- /dev/null +++ b/tests/snapshots/cli_matrix__row_011_nowrap_stdout.snap @@ -0,0 +1,25 @@ +--- +source: tests/cli_matrix.rs +expression: stdout_result.envelope(&stdout_case) +--- +case: row_011_nowrap +mode: stdout +args: --breaks --ellipsis --fences --footnotes +status: exit status: 0 + +[stdout] +--- +title: Matrix +--- + +Heading +______________________________________________________________________ + +______________________________________________________________________ + +Text… after break. + +[stderr] + +[file] + diff --git a/tests/snapshots/cli_matrix__row_011_wrap_in_place.snap b/tests/snapshots/cli_matrix__row_011_wrap_in_place.snap new file mode 100644 index 00000000..f2d8005c --- /dev/null +++ b/tests/snapshots/cli_matrix__row_011_wrap_in_place.snap @@ -0,0 +1,24 @@ +--- +source: tests/cli_matrix.rs +expression: in_place_result.envelope(&in_place_case) +--- +case: row_011_wrap +mode: in_place +args: --wrap --breaks --ellipsis --fences --footnotes --in-place +status: exit status: 0 + +[stdout] + +[stderr] + +[file] +--- +title: Matrix +--- + +Heading +______________________________________________________________________ + +______________________________________________________________________ + +Text… after break. diff --git a/tests/snapshots/cli_matrix__row_011_wrap_stdout.snap b/tests/snapshots/cli_matrix__row_011_wrap_stdout.snap new file mode 100644 index 00000000..44698995 --- /dev/null +++ b/tests/snapshots/cli_matrix__row_011_wrap_stdout.snap @@ -0,0 +1,25 @@ +--- +source: tests/cli_matrix.rs +expression: stdout_result.envelope(&stdout_case) +--- +case: row_011_wrap +mode: stdout +args: --wrap --breaks --ellipsis --fences --footnotes +status: exit status: 0 + +[stdout] +--- +title: Matrix +--- + +Heading +______________________________________________________________________ + +______________________________________________________________________ + +Text… after break. + +[stderr] + +[file] + diff --git a/tests/snapshots/cli_matrix__row_100_nowrap_in_place.snap b/tests/snapshots/cli_matrix__row_100_nowrap_in_place.snap new file mode 100644 index 00000000..b60a7b1d --- /dev/null +++ b/tests/snapshots/cli_matrix__row_100_nowrap_in_place.snap @@ -0,0 +1,22 @@ +--- +source: tests/cli_matrix.rs +expression: in_place_result.envelope(&in_place_case) +--- +case: row_100_nowrap +mode: in_place +args: --renumber --fences --footnotes --headings --in-place +status: exit status: 0 + +[stdout] + +[stderr] + +[file] +| Name | Notes | +| ----- | ------------------------------------ | +| alpha | Use *`cargo test`* before merging... | + +1. first item +2. second item with enough text to wrap around the configured column width when wrapping is enabled for the matrix harness. + +# Title diff --git a/tests/snapshots/cli_matrix__row_100_nowrap_stdout.snap b/tests/snapshots/cli_matrix__row_100_nowrap_stdout.snap new file mode 100644 index 00000000..888478ff --- /dev/null +++ b/tests/snapshots/cli_matrix__row_100_nowrap_stdout.snap @@ -0,0 +1,23 @@ +--- +source: tests/cli_matrix.rs +expression: stdout_result.envelope(&stdout_case) +--- +case: row_100_nowrap +mode: stdout +args: --renumber --fences --footnotes --headings +status: exit status: 0 + +[stdout] +| Name | Notes | +| ----- | ------------------------------------ | +| alpha | Use *`cargo test`* before merging... | + +1. first item +2. second item with enough text to wrap around the configured column width when wrapping is enabled for the matrix harness. + +# Title + +[stderr] + +[file] + diff --git a/tests/snapshots/cli_matrix__row_100_wrap_in_place.snap b/tests/snapshots/cli_matrix__row_100_wrap_in_place.snap new file mode 100644 index 00000000..0d772e1e --- /dev/null +++ b/tests/snapshots/cli_matrix__row_100_wrap_in_place.snap @@ -0,0 +1,23 @@ +--- +source: tests/cli_matrix.rs +expression: in_place_result.envelope(&in_place_case) +--- +case: row_100_wrap +mode: in_place +args: --wrap --renumber --fences --footnotes --headings --in-place +status: exit status: 0 + +[stdout] + +[stderr] + +[file] +| Name | Notes | +| ----- | ------------------------------------ | +| alpha | Use *`cargo test`* before merging... | + +1. first item +2. second item with enough text to wrap around the configured column width when + wrapping is enabled for the matrix harness. + +# Title diff --git a/tests/snapshots/cli_matrix__row_100_wrap_stdout.snap b/tests/snapshots/cli_matrix__row_100_wrap_stdout.snap new file mode 100644 index 00000000..45550752 --- /dev/null +++ b/tests/snapshots/cli_matrix__row_100_wrap_stdout.snap @@ -0,0 +1,24 @@ +--- +source: tests/cli_matrix.rs +expression: stdout_result.envelope(&stdout_case) +--- +case: row_100_wrap +mode: stdout +args: --wrap --renumber --fences --footnotes --headings +status: exit status: 0 + +[stdout] +| Name | Notes | +| ----- | ------------------------------------ | +| alpha | Use *`cargo test`* before merging... | + +1. first item +2. second item with enough text to wrap around the configured column width when + wrapping is enabled for the matrix harness. + +# Title + +[stderr] + +[file] + diff --git a/tests/snapshots/cli_matrix__row_101_nowrap_in_place.snap b/tests/snapshots/cli_matrix__row_101_nowrap_in_place.snap new file mode 100644 index 00000000..109a8c4e --- /dev/null +++ b/tests/snapshots/cli_matrix__row_101_nowrap_in_place.snap @@ -0,0 +1,24 @@ +--- +source: tests/cli_matrix.rs +expression: in_place_result.envelope(&in_place_case) +--- +case: row_101_nowrap +mode: in_place +args: --renumber --ellipsis --fences --code-emphasis --in-place +status: exit status: 0 + +[stdout] + +[stderr] + +[file] +```rust +fn main() { + println!("dots..."); +} +``` + +Outside… text. + +1. first +2. second diff --git a/tests/snapshots/cli_matrix__row_101_nowrap_stdout.snap b/tests/snapshots/cli_matrix__row_101_nowrap_stdout.snap new file mode 100644 index 00000000..2e8e89c4 --- /dev/null +++ b/tests/snapshots/cli_matrix__row_101_nowrap_stdout.snap @@ -0,0 +1,25 @@ +--- +source: tests/cli_matrix.rs +expression: stdout_result.envelope(&stdout_case) +--- +case: row_101_nowrap +mode: stdout +args: --renumber --ellipsis --fences --code-emphasis +status: exit status: 0 + +[stdout] +```rust +fn main() { + println!("dots..."); +} +``` + +Outside… text. + +1. first +2. second + +[stderr] + +[file] + diff --git a/tests/snapshots/cli_matrix__row_101_wrap_in_place.snap b/tests/snapshots/cli_matrix__row_101_wrap_in_place.snap new file mode 100644 index 00000000..2067614e --- /dev/null +++ b/tests/snapshots/cli_matrix__row_101_wrap_in_place.snap @@ -0,0 +1,24 @@ +--- +source: tests/cli_matrix.rs +expression: in_place_result.envelope(&in_place_case) +--- +case: row_101_wrap +mode: in_place +args: --wrap --renumber --ellipsis --fences --code-emphasis --in-place +status: exit status: 0 + +[stdout] + +[stderr] + +[file] +```rust +fn main() { + println!("dots..."); +} +``` + +Outside… text. + +1. first +2. second diff --git a/tests/snapshots/cli_matrix__row_101_wrap_stdout.snap b/tests/snapshots/cli_matrix__row_101_wrap_stdout.snap new file mode 100644 index 00000000..aeedd99a --- /dev/null +++ b/tests/snapshots/cli_matrix__row_101_wrap_stdout.snap @@ -0,0 +1,25 @@ +--- +source: tests/cli_matrix.rs +expression: stdout_result.envelope(&stdout_case) +--- +case: row_101_wrap +mode: stdout +args: --wrap --renumber --ellipsis --fences --code-emphasis +status: exit status: 0 + +[stdout] +```rust +fn main() { + println!("dots..."); +} +``` + +Outside… text. + +1. first +2. second + +[stderr] + +[file] + diff --git a/tests/snapshots/cli_matrix__row_110_nowrap_in_place.snap b/tests/snapshots/cli_matrix__row_110_nowrap_in_place.snap new file mode 100644 index 00000000..f1164a15 --- /dev/null +++ b/tests/snapshots/cli_matrix__row_110_nowrap_in_place.snap @@ -0,0 +1,18 @@ +--- +source: tests/cli_matrix.rs +expression: in_place_result.envelope(&in_place_case) +--- +case: row_110_nowrap +mode: in_place +args: --renumber --breaks --footnotes --code-emphasis --in-place +status: exit status: 0 + +[stdout] + +[stderr] + +[file] +This paragraph references item 1 and uses enough words to demonstrate wrapping behaviour when the wrap variant is enabled. + +1. https://example.com/alpha +2. https://example.com/beta diff --git a/tests/snapshots/cli_matrix__row_110_nowrap_stdout.snap b/tests/snapshots/cli_matrix__row_110_nowrap_stdout.snap new file mode 100644 index 00000000..9ed0dc8e --- /dev/null +++ b/tests/snapshots/cli_matrix__row_110_nowrap_stdout.snap @@ -0,0 +1,19 @@ +--- +source: tests/cli_matrix.rs +expression: stdout_result.envelope(&stdout_case) +--- +case: row_110_nowrap +mode: stdout +args: --renumber --breaks --footnotes --code-emphasis +status: exit status: 0 + +[stdout] +This paragraph references item 1 and uses enough words to demonstrate wrapping behaviour when the wrap variant is enabled. + +1. https://example.com/alpha +2. https://example.com/beta + +[stderr] + +[file] + diff --git a/tests/snapshots/cli_matrix__row_110_wrap_in_place.snap b/tests/snapshots/cli_matrix__row_110_wrap_in_place.snap new file mode 100644 index 00000000..7bfb6954 --- /dev/null +++ b/tests/snapshots/cli_matrix__row_110_wrap_in_place.snap @@ -0,0 +1,19 @@ +--- +source: tests/cli_matrix.rs +expression: in_place_result.envelope(&in_place_case) +--- +case: row_110_wrap +mode: in_place +args: --wrap --renumber --breaks --footnotes --code-emphasis --in-place +status: exit status: 0 + +[stdout] + +[stderr] + +[file] +This paragraph references item 1 and uses enough words to demonstrate wrapping +behaviour when the wrap variant is enabled. + +1. https://example.com/alpha +2. https://example.com/beta diff --git a/tests/snapshots/cli_matrix__row_110_wrap_stdout.snap b/tests/snapshots/cli_matrix__row_110_wrap_stdout.snap new file mode 100644 index 00000000..4e8e9be2 --- /dev/null +++ b/tests/snapshots/cli_matrix__row_110_wrap_stdout.snap @@ -0,0 +1,20 @@ +--- +source: tests/cli_matrix.rs +expression: stdout_result.envelope(&stdout_case) +--- +case: row_110_wrap +mode: stdout +args: --wrap --renumber --breaks --footnotes --code-emphasis +status: exit status: 0 + +[stdout] +This paragraph references item 1 and uses enough words to demonstrate wrapping +behaviour when the wrap variant is enabled. + +1. https://example.com/alpha +2. https://example.com/beta + +[stderr] + +[file] + diff --git a/tests/snapshots/cli_matrix__row_111_nowrap_in_place.snap b/tests/snapshots/cli_matrix__row_111_nowrap_in_place.snap new file mode 100644 index 00000000..e1be2bbe --- /dev/null +++ b/tests/snapshots/cli_matrix__row_111_nowrap_in_place.snap @@ -0,0 +1,23 @@ +--- +source: tests/cli_matrix.rs +expression: in_place_result.envelope(&in_place_case) +--- +case: row_111_nowrap +mode: in_place +args: --renumber --breaks --ellipsis --headings --in-place +status: exit status: 0 + +[stdout] + +[stderr] + +[file] +--- +title: Matrix +--- + +## Heading + +______________________________________________________________________ + +Text… after break. diff --git a/tests/snapshots/cli_matrix__row_111_nowrap_stdout.snap b/tests/snapshots/cli_matrix__row_111_nowrap_stdout.snap new file mode 100644 index 00000000..0009cfc1 --- /dev/null +++ b/tests/snapshots/cli_matrix__row_111_nowrap_stdout.snap @@ -0,0 +1,24 @@ +--- +source: tests/cli_matrix.rs +expression: stdout_result.envelope(&stdout_case) +--- +case: row_111_nowrap +mode: stdout +args: --renumber --breaks --ellipsis --headings +status: exit status: 0 + +[stdout] +--- +title: Matrix +--- + +## Heading + +______________________________________________________________________ + +Text… after break. + +[stderr] + +[file] + diff --git a/tests/snapshots/cli_matrix__row_111_wrap_in_place.snap b/tests/snapshots/cli_matrix__row_111_wrap_in_place.snap new file mode 100644 index 00000000..738526a1 --- /dev/null +++ b/tests/snapshots/cli_matrix__row_111_wrap_in_place.snap @@ -0,0 +1,23 @@ +--- +source: tests/cli_matrix.rs +expression: in_place_result.envelope(&in_place_case) +--- +case: row_111_wrap +mode: in_place +args: --wrap --renumber --breaks --ellipsis --headings --in-place +status: exit status: 0 + +[stdout] + +[stderr] + +[file] +--- +title: Matrix +--- + +## Heading + +______________________________________________________________________ + +Text… after break. diff --git a/tests/snapshots/cli_matrix__row_111_wrap_stdout.snap b/tests/snapshots/cli_matrix__row_111_wrap_stdout.snap new file mode 100644 index 00000000..a7f85d83 --- /dev/null +++ b/tests/snapshots/cli_matrix__row_111_wrap_stdout.snap @@ -0,0 +1,24 @@ +--- +source: tests/cli_matrix.rs +expression: stdout_result.envelope(&stdout_case) +--- +case: row_111_wrap +mode: stdout +args: --wrap --renumber --breaks --ellipsis --headings +status: exit status: 0 + +[stdout] +--- +title: Matrix +--- + +## Heading + +______________________________________________________________________ + +Text… after break. + +[stderr] + +[file] +