From e56f32a753a114d76d0409b5317ac09556f411e9 Mon Sep 17 00:00:00 2001 From: leynos Date: Sat, 25 Jul 2026 15:30:31 +0200 Subject: [PATCH 1/5] Add tracing event snapshots Capture stable tracing output for inline fragment, parser, and date-span\nclassification events. The snapshots preserve level, target, message, and\nstructured fields while excluding volatile prefixes. --- src/wrap.rs | 2 + src/wrap/inline/fragment.rs | 30 +++++++++++++ ...oken-span-grouped-date-sequence-event.snap | 5 +++ .../snapshots/fragment-classified-event.snap | 5 +++ .../matched-date-sequence-event.snap | 5 +++ src/wrap/inline/span_helper_tracing_tests.rs | 43 +++++++++++++++++- src/wrap/tokenize/parsing.rs | 4 ++ .../tokenize/parsing_tracing_snapshots.rs | 44 +++++++++++++++++++ .../footnote-reference-parsed-event.snap | 5 +++ .../snapshots/link-or-image-parsed-event.snap | 5 +++ src/wrap/tracing_snapshot_support.rs | 35 +++++++++++++++ 11 files changed, 182 insertions(+), 1 deletion(-) create mode 100644 src/wrap/inline/snapshots/determine-token-span-grouped-date-sequence-event.snap create mode 100644 src/wrap/inline/snapshots/fragment-classified-event.snap create mode 100644 src/wrap/inline/snapshots/matched-date-sequence-event.snap create mode 100644 src/wrap/tokenize/parsing_tracing_snapshots.rs create mode 100644 src/wrap/tokenize/snapshots/footnote-reference-parsed-event.snap create mode 100644 src/wrap/tokenize/snapshots/link-or-image-parsed-event.snap create mode 100644 src/wrap/tracing_snapshot_support.rs diff --git a/src/wrap.rs b/src/wrap.rs index b71a3a12..ec7e0777 100644 --- a/src/wrap.rs +++ b/src/wrap.rs @@ -20,6 +20,8 @@ mod inline; mod link_reference; mod paragraph; mod tokenize; +#[cfg(test)] +pub(crate) mod tracing_snapshot_support; use block::{BULLET_RE, FOOTNOTE_RE}; pub(crate) use block::{BlockKind, classify_block, leading_indent}; pub use blockquote::BlockquotePrefix; diff --git a/src/wrap/inline/fragment.rs b/src/wrap/inline/fragment.rs index ce6c42bb..73ec1e1b 100644 --- a/src/wrap/inline/fragment.rs +++ b/src/wrap/inline/fragment.rs @@ -370,6 +370,36 @@ mod tracing_tests { } } +#[cfg(test)] +mod tracing_snapshots { + //! Snapshot test for fragment-classification tracing events. + + use std::cell::RefCell; + + use tracing_test::traced_test; + + use super::InlineFragment; + use crate::wrap::tracing_snapshot_support::normalise_event_lines; + + #[traced_test] + #[test] + fn snapshots_fragment_classified_event() { + let captured = RefCell::new(String::new()); + let _ = InlineFragment::new("plain".to_string()); + logs_assert(|lines| { + captured.replace(normalise_event_lines(lines, "fragment classified")); + (!captured.borrow().is_empty()) + .then_some(()) + .ok_or_else(|| "expected fragment classification event".to_string()) + }); + let event = captured.into_inner(); + + insta::with_settings!({prepend_module_to_snapshot => false}, { + insta::assert_snapshot!("fragment-classified-event", event); + }); + } +} + #[cfg(test)] mod proptests { //! Property tests for `trace_text_snippet` invariants. diff --git a/src/wrap/inline/snapshots/determine-token-span-grouped-date-sequence-event.snap b/src/wrap/inline/snapshots/determine-token-span-grouped-date-sequence-event.snap new file mode 100644 index 00000000..a976fc54 --- /dev/null +++ b/src/wrap/inline/snapshots/determine-token-span-grouped-date-sequence-event.snap @@ -0,0 +1,5 @@ +--- +source: src/wrap/inline/span_helper_tracing_tests.rs +expression: event +--- +TRACE snapshots_grouped_date_sequence_event: mdtablefix::wrap::inline: determine_token_span grouped date sequence start=0 end=5 width=18 diff --git a/src/wrap/inline/snapshots/fragment-classified-event.snap b/src/wrap/inline/snapshots/fragment-classified-event.snap new file mode 100644 index 00000000..0c926723 --- /dev/null +++ b/src/wrap/inline/snapshots/fragment-classified-event.snap @@ -0,0 +1,5 @@ +--- +source: src/wrap/inline/fragment.rs +expression: event +--- +DEBUG snapshots_fragment_classified_event: mdtablefix::wrap::inline::fragment: fragment classified token=plain truncated=false kind=Plain diff --git a/src/wrap/inline/snapshots/matched-date-sequence-event.snap b/src/wrap/inline/snapshots/matched-date-sequence-event.snap new file mode 100644 index 00000000..f6a826cb --- /dev/null +++ b/src/wrap/inline/snapshots/matched-date-sequence-event.snap @@ -0,0 +1,5 @@ +--- +source: src/wrap/inline/span_helper_tracing_tests.rs +expression: event +--- +DEBUG snapshots_matched_date_sequence_event:date_token_span{start=0}:try_match_date_sequence{start=0}: mdtablefix::wrap::inline::span_helpers: matched date sequence start=0 end=5 pattern="ordinal_day_month_year" diff --git a/src/wrap/inline/span_helper_tracing_tests.rs b/src/wrap/inline/span_helper_tracing_tests.rs index 4aefff6a..78155c48 100644 --- a/src/wrap/inline/span_helper_tracing_tests.rs +++ b/src/wrap/inline/span_helper_tracing_tests.rs @@ -1,10 +1,12 @@ //! Traced-event tests for inline span helper instrumentation. +use std::cell::RefCell; + use rstest::{fixture, rstest}; use tracing_test::traced_test; use super::{date_token_span, try_match_date_sequence}; -use crate::wrap::inline::determine_token_span; +use crate::wrap::{inline::determine_token_span, tracing_snapshot_support::normalise_event_lines}; #[fixture] fn date_tokens() -> Vec { @@ -87,3 +89,42 @@ fn grouping_boundary_logs_declined_footnote_coupling( ))); assert!(!logs_contain("[^note]")); } + +#[traced_test] +#[rstest] +fn snapshots_grouped_date_sequence_event(date_tokens: Vec) { + let captured = RefCell::new(String::new()); + let _ = determine_token_span(&date_tokens, 0); + logs_assert(|lines| { + captured.replace(normalise_event_lines( + lines, + "determine_token_span grouped date sequence", + )); + (!captured.borrow().is_empty()) + .then_some(()) + .ok_or_else(|| "expected grouped date sequence event".to_string()) + }); + let event = captured.into_inner(); + + insta::with_settings!({prepend_module_to_snapshot => false}, { + insta::assert_snapshot!("determine-token-span-grouped-date-sequence-event", event); + }); +} + +#[traced_test] +#[rstest] +fn snapshots_matched_date_sequence_event(date_tokens: Vec) { + let captured = RefCell::new(String::new()); + let _ = determine_token_span(&date_tokens, 0); + logs_assert(|lines| { + captured.replace(normalise_event_lines(lines, "matched date sequence")); + (!captured.borrow().is_empty()) + .then_some(()) + .ok_or_else(|| "expected matched date sequence event".to_string()) + }); + let event = captured.into_inner(); + + insta::with_settings!({prepend_module_to_snapshot => false}, { + insta::assert_snapshot!("matched-date-sequence-event", event); + }); +} diff --git a/src/wrap/tokenize/parsing.rs b/src/wrap/tokenize/parsing.rs index 90156766..39877312 100644 --- a/src/wrap/tokenize/parsing.rs +++ b/src/wrap/tokenize/parsing.rs @@ -221,3 +221,7 @@ pub(super) fn handle_backtick_fence(text: &str, start_idx: usize) -> (String, us #[cfg(test)] #[path = "parsing_tests.rs"] mod tests; + +#[cfg(test)] +#[path = "parsing_tracing_snapshots.rs"] +mod parsing_tracing_snapshots; diff --git a/src/wrap/tokenize/parsing_tracing_snapshots.rs b/src/wrap/tokenize/parsing_tracing_snapshots.rs new file mode 100644 index 00000000..9ab33493 --- /dev/null +++ b/src/wrap/tokenize/parsing_tracing_snapshots.rs @@ -0,0 +1,44 @@ +//! Snapshot tests for parsing tracing events. + +use std::cell::RefCell; + +use tracing_test::traced_test; + +use super::parse_link_or_image; +use crate::wrap::tracing_snapshot_support::normalise_event_lines; + +#[traced_test] +#[test] +fn snapshots_footnote_reference_parsed_event() { + let captured = RefCell::new(String::new()); + let _ = parse_link_or_image("[^4] tail", 0); + logs_assert(|lines| { + captured.replace(normalise_event_lines(lines, "footnote reference parsed")); + (!captured.borrow().is_empty()) + .then_some(()) + .ok_or_else(|| "expected footnote reference parsed event".to_string()) + }); + let event = captured.into_inner(); + + insta::with_settings!({prepend_module_to_snapshot => false}, { + insta::assert_snapshot!("footnote-reference-parsed-event", event); + }); +} + +#[traced_test] +#[test] +fn snapshots_link_or_image_parsed_event() { + let captured = RefCell::new(String::new()); + let _ = parse_link_or_image("[link](url)", 0); + logs_assert(|lines| { + captured.replace(normalise_event_lines(lines, "link or image parsed")); + (!captured.borrow().is_empty()) + .then_some(()) + .ok_or_else(|| "expected link or image parsed event".to_string()) + }); + let event = captured.into_inner(); + + insta::with_settings!({prepend_module_to_snapshot => false}, { + insta::assert_snapshot!("link-or-image-parsed-event", event); + }); +} diff --git a/src/wrap/tokenize/snapshots/footnote-reference-parsed-event.snap b/src/wrap/tokenize/snapshots/footnote-reference-parsed-event.snap new file mode 100644 index 00000000..163a322a --- /dev/null +++ b/src/wrap/tokenize/snapshots/footnote-reference-parsed-event.snap @@ -0,0 +1,5 @@ +--- +source: src/wrap/tokenize/parsing_tracing_snapshots.rs +expression: event +--- +DEBUG snapshots_footnote_reference_parsed_event:parse_link_or_image{idx=0}: mdtablefix::wrap::tokenize::parsing: footnote reference parsed token_length=4 diff --git a/src/wrap/tokenize/snapshots/link-or-image-parsed-event.snap b/src/wrap/tokenize/snapshots/link-or-image-parsed-event.snap new file mode 100644 index 00000000..b9a0927c --- /dev/null +++ b/src/wrap/tokenize/snapshots/link-or-image-parsed-event.snap @@ -0,0 +1,5 @@ +--- +source: src/wrap/tokenize/parsing_tracing_snapshots.rs +expression: event +--- +DEBUG snapshots_link_or_image_parsed_event:parse_link_or_image{idx=0}: mdtablefix::wrap::tokenize::parsing: link or image parsed token_length=11 is_image=false diff --git a/src/wrap/tracing_snapshot_support.rs b/src/wrap/tracing_snapshot_support.rs new file mode 100644 index 00000000..28612daa --- /dev/null +++ b/src/wrap/tracing_snapshot_support.rs @@ -0,0 +1,35 @@ +//! Stable capture support for tracing-event snapshots. +//! +//! Call this helper inside a `#[traced_test]` function through its injected +//! `logs_assert` closure. Copy the normalized result into an owned buffer +//! before passing it to `insta::assert_snapshot!` after the closure returns. + +/// Normalizes captured tracing lines for a single event message. +/// +/// Retains only lines containing `message`, removes the volatile prefix before +/// the event level, trims trailing whitespace, and joins the remaining lines. +/// The level, target, message, and structured fields remain intact. +pub(crate) fn normalise_event_lines(lines: &[&str], message: &str) -> String { + lines + .iter() + .filter(|line| line.contains(message)) + .map(|line| stable_event_start(line).trim_end()) + .collect::>() + .join("\n") +} + +fn stable_event_start(line: &str) -> &str { + ["TRACE", "DEBUG"] + .iter() + .filter_map(|level| { + line.find(level).filter(|&index| { + index == 0 + || line[..index] + .chars() + .next_back() + .is_some_and(char::is_whitespace) + }) + }) + .min() + .map_or(line, |index| &line[index..]) +} From c55f13fef565e11f3e5514c30719ee3bdf662dad Mon Sep 17 00:00:00 2001 From: leynos Date: Sun, 26 Jul 2026 19:11:05 +0200 Subject: [PATCH 2/5] Address review feedback for tracing snapshots Move the fragment-classification snapshot test out of the oversized `fragment.rs` (which had grown past the 400-line limit) into a dedicated `fragment_tracing_snapshots.rs`, wired as a `#[path]` submodule to mirror the tokenizer snapshot layout. Add a usage example to the shared `normalise_event_lines` helper showing the `tracing-test` line format and its normalized output. Document the tracing-event snapshot-testing workflow and record the shared helper's ownership boundaries, permitted call-sites, and composition rules in docs/developers-guide.md. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/developers-guide.md | 46 +++++++++++++++++++ src/wrap/inline/fragment.rs | 33 ++----------- src/wrap/inline/fragment_tracing_snapshots.rs | 26 +++++++++++ .../snapshots/fragment-classified-event.snap | 2 +- src/wrap/tracing_snapshot_support.rs | 16 +++++++ 5 files changed, 92 insertions(+), 31 deletions(-) create mode 100644 src/wrap/inline/fragment_tracing_snapshots.rs diff --git a/docs/developers-guide.md b/docs/developers-guide.md index b356cbaf..4a994aff 100644 --- a/docs/developers-guide.md +++ b/docs/developers-guide.md @@ -659,6 +659,52 @@ Table: Instrumented functions and their logging levels and fields. | `find_footnote_end` | trace | `idx` (in), `skip(text)`, return value (out) | | `parse_rows` | trace, debug | `skip(trimmed)`; `row_index`, `cell_count`, and `error_category` events | +### Tracing-event snapshot tests + +The stable structured fields above are pinned by `insta` snapshots so that +accidental changes to a tracing event's level, target, message, or field set +are caught in review. These tests live next to the instrumented code: + +- `src/wrap/inline/fragment_tracing_snapshots.rs` – `fragment classified`. +- `src/wrap/inline/span_helper_tracing_tests.rs` – date-span events. +- `src/wrap/tokenize/parsing_tracing_snapshots.rs` – link, image, and footnote + events. + +Each is wired into its owning module as a `#[cfg(test)]` `#[path = "…"]` +submodule so the snapshot test sits beside the code it pins while keeping the +production module within the 400-line limit. The `.snap` fixtures live under the +neighbouring `snapshots/` directory. + +A test captures events with `tracing-test`'s `#[traced_test]`, then normalizes +the captured lines through the shared +`crate::wrap::tracing_snapshot_support::normalise_event_lines` helper before +asserting the snapshot. + +#### `normalise_event_lines` helper + +`tracing-test` prefixes every captured line with a volatile timestamp and span +context, which would make raw snapshots non-deterministic. +`normalise_event_lines(lines, message)` retains only the lines containing +`message`, strips the volatile prefix up to the event level (`TRACE`/`DEBUG`), +trims trailing whitespace, and joins the survivors. The level, target, message, +and structured fields are preserved verbatim, so the snapshot still fails if any +of those change. + +Re-use policy for this helper: + +- **Ownership.** Owned by the wrap module (`src/wrap/tracing_snapshot_support.rs`) + and gated behind `#[cfg(test)]`; it is `pub(crate)` test-support code, not part + of any public or runtime API. +- **Permitted call-sites.** Only tracing-event snapshot tests. Call it from + inside a `#[traced_test]` function through the injected `logs_assert` closure, + copying the normalized result into an owned buffer before asserting the + snapshot after the closure returns (see the modules listed above for the + canonical shape). +- **Composition.** Do not layer additional normalization on top; if a new event + needs different masking, extend the helper (and this section) rather than + post-processing its output at the call-site, so every snapshot shares one + normalization contract. + ## Fences module The `fences` module in [src/fences.rs](../src/fences.rs) is responsible for diff --git a/src/wrap/inline/fragment.rs b/src/wrap/inline/fragment.rs index 73ec1e1b..ece23e2f 100644 --- a/src/wrap/inline/fragment.rs +++ b/src/wrap/inline/fragment.rs @@ -370,36 +370,6 @@ mod tracing_tests { } } -#[cfg(test)] -mod tracing_snapshots { - //! Snapshot test for fragment-classification tracing events. - - use std::cell::RefCell; - - use tracing_test::traced_test; - - use super::InlineFragment; - use crate::wrap::tracing_snapshot_support::normalise_event_lines; - - #[traced_test] - #[test] - fn snapshots_fragment_classified_event() { - let captured = RefCell::new(String::new()); - let _ = InlineFragment::new("plain".to_string()); - logs_assert(|lines| { - captured.replace(normalise_event_lines(lines, "fragment classified")); - (!captured.borrow().is_empty()) - .then_some(()) - .ok_or_else(|| "expected fragment classification event".to_string()) - }); - let event = captured.into_inner(); - - insta::with_settings!({prepend_module_to_snapshot => false}, { - insta::assert_snapshot!("fragment-classified-event", event); - }); - } -} - #[cfg(test)] mod proptests { //! Property tests for `trace_text_snippet` invariants. @@ -425,3 +395,6 @@ mod proptests { } } } +#[cfg(test)] +#[path = "fragment_tracing_snapshots.rs"] +mod fragment_tracing_snapshots; diff --git a/src/wrap/inline/fragment_tracing_snapshots.rs b/src/wrap/inline/fragment_tracing_snapshots.rs new file mode 100644 index 00000000..50c69767 --- /dev/null +++ b/src/wrap/inline/fragment_tracing_snapshots.rs @@ -0,0 +1,26 @@ +//! Snapshot test for fragment-classification tracing events. + +use std::cell::RefCell; + +use tracing_test::traced_test; + +use super::InlineFragment; +use crate::wrap::tracing_snapshot_support::normalise_event_lines; + +#[traced_test] +#[test] +fn snapshots_fragment_classified_event() { + let captured = RefCell::new(String::new()); + let _ = InlineFragment::new("plain".to_string()); + logs_assert(|lines| { + captured.replace(normalise_event_lines(lines, "fragment classified")); + (!captured.borrow().is_empty()) + .then_some(()) + .ok_or_else(|| "expected fragment classification event".to_string()) + }); + let event = captured.into_inner(); + + insta::with_settings!({prepend_module_to_snapshot => false}, { + insta::assert_snapshot!("fragment-classified-event", event); + }); +} diff --git a/src/wrap/inline/snapshots/fragment-classified-event.snap b/src/wrap/inline/snapshots/fragment-classified-event.snap index 0c926723..b3fc1859 100644 --- a/src/wrap/inline/snapshots/fragment-classified-event.snap +++ b/src/wrap/inline/snapshots/fragment-classified-event.snap @@ -1,5 +1,5 @@ --- -source: src/wrap/inline/fragment.rs +source: src/wrap/inline/fragment_tracing_snapshots.rs expression: event --- DEBUG snapshots_fragment_classified_event: mdtablefix::wrap::inline::fragment: fragment classified token=plain truncated=false kind=Plain diff --git a/src/wrap/tracing_snapshot_support.rs b/src/wrap/tracing_snapshot_support.rs index 28612daa..8f834239 100644 --- a/src/wrap/tracing_snapshot_support.rs +++ b/src/wrap/tracing_snapshot_support.rs @@ -9,6 +9,22 @@ /// Retains only lines containing `message`, removes the volatile prefix before /// the event level, trims trailing whitespace, and joins the remaining lines. /// The level, target, message, and structured fields remain intact. +/// +/// # Examples +/// +/// ```ignore +/// // `tracing-test` prepends a volatile timestamp and span context before the +/// // level; only the level onward is stable enough to snapshot. +/// let lines = [ +/// "2026-07-26T00:00:00Z DEBUG snapshots: mdtablefix::wrap: fragment classified kind=Plain ", +/// "2026-07-26T00:00:00Z TRACE snapshots: mdtablefix::wrap: predicate matched", +/// ]; +/// let normalized = normalise_event_lines(&lines, "fragment classified"); +/// assert_eq!( +/// normalized, +/// "DEBUG snapshots: mdtablefix::wrap: fragment classified kind=Plain", +/// ); +/// ``` pub(crate) fn normalise_event_lines(lines: &[&str], message: &str) -> String { lines .iter() From a004a776901c31be24cf9d4e8fd6035937ce6f83 Mon Sep 17 00:00:00 2001 From: leynos Date: Mon, 27 Jul 2026 00:03:50 +0200 Subject: [PATCH 3/5] Add behavioural and property coverage for tracing snapshots Respond to review feedback that the tracing snapshot tests only pinned internal trace output. The snapshot and event tests now also assert the observable parsing, classification, and span-grouping results (link/footnote atomicity, plain-fragment classification, date-sequence grouping and width, and the declined-coupling edge case), keeping the trace snapshots supplementary rather than primary. Add proptest coverage for the shared normalisation helper: prove `stable_event_start` returns an idempotent suffix and strips a synthetic volatile prefix while preserving the event level and fields, and that `normalise_event_lines` keeps exactly the matching lines without trailing whitespace. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/wrap/inline/fragment_tracing_snapshots.rs | 6 +- src/wrap/inline/span_helper_tracing_tests.rs | 28 +++++++-- .../tokenize/parsing_tracing_snapshots.rs | 12 +++- src/wrap/tracing_snapshot_support.rs | 59 +++++++++++++++++++ 4 files changed, 96 insertions(+), 9 deletions(-) diff --git a/src/wrap/inline/fragment_tracing_snapshots.rs b/src/wrap/inline/fragment_tracing_snapshots.rs index 50c69767..32bc5997 100644 --- a/src/wrap/inline/fragment_tracing_snapshots.rs +++ b/src/wrap/inline/fragment_tracing_snapshots.rs @@ -11,7 +11,11 @@ use crate::wrap::tracing_snapshot_support::normalise_event_lines; #[test] fn snapshots_fragment_classified_event() { let captured = RefCell::new(String::new()); - let _ = InlineFragment::new("plain".to_string()); + let fragment = InlineFragment::new("plain".to_string()); + // Assert the observable classification the traced event mirrors; the + // snapshot below is supplementary evidence of the emitted event shape. + assert!(fragment.is_plain(), "a bare word must classify as Plain"); + assert_eq!(fragment.text, "plain"); logs_assert(|lines| { captured.replace(normalise_event_lines(lines, "fragment classified")); (!captured.borrow().is_empty()) diff --git a/src/wrap/inline/span_helper_tracing_tests.rs b/src/wrap/inline/span_helper_tracing_tests.rs index 78155c48..cfdcf187 100644 --- a/src/wrap/inline/span_helper_tracing_tests.rs +++ b/src/wrap/inline/span_helper_tracing_tests.rs @@ -32,21 +32,24 @@ fn colon_footnote_tokens() -> Vec { #[traced_test] #[rstest] fn try_match_date_sequence_emits_trace_event(date_tokens: Vec) { - let _ = try_match_date_sequence(&date_tokens, 0); + // Observable result: all five date tokens form one sequence (exclusive end). + assert_eq!(try_match_date_sequence(&date_tokens, 0), Some(5)); assert!(logs_contain("try_match_date_sequence")); } #[traced_test] #[rstest] fn try_match_date_sequence_logs_matched_pattern(date_tokens: Vec) { - let _ = try_match_date_sequence(&date_tokens, 0); + assert_eq!(try_match_date_sequence(&date_tokens, 0), Some(5)); assert!(logs_contain("ordinal_day_month_year")); } #[traced_test] #[rstest] fn date_token_span_emits_trace_event(date_tokens: Vec) { - let _ = date_token_span(&date_tokens, 0); + // Observable result: the span covers all five tokens with display width 18 + // ("25th December 2025"); no footnote coupling applies here. + assert_eq!(date_token_span(&date_tokens, 0), Some((5, 18))); assert!(logs_contain("date_token_span")); } @@ -83,7 +86,13 @@ fn grouping_boundary_logs_declined_footnote_coupling( .iter() .map(|token| (*token).to_string()) .collect::>(); - let _ = determine_token_span(&tokens, 0); + let (end, _width) = determine_token_span(&tokens, 0); + // Observable edge-case result: a declined coupling leaves the footnote + // reference outside the returned span rather than grouping it. + assert!( + !tokens[..end].join("").contains("[^note]"), + "declined coupling must not group the footnote reference", + ); assert!(logs_contain(&format!( "error_category=\"{error_category}\"" ))); @@ -94,7 +103,11 @@ fn grouping_boundary_logs_declined_footnote_coupling( #[rstest] fn snapshots_grouped_date_sequence_event(date_tokens: Vec) { let captured = RefCell::new(String::new()); - let _ = determine_token_span(&date_tokens, 0); + // Observable grouping result: the date tokens form one atomic span of + // width 18. The snapshot below supplements this behavioural assertion. + let (end, width) = determine_token_span(&date_tokens, 0); + assert_eq!(date_tokens[..end].join(""), "25th December 2025"); + assert_eq!(width, 18); logs_assert(|lines| { captured.replace(normalise_event_lines( lines, @@ -115,7 +128,10 @@ fn snapshots_grouped_date_sequence_event(date_tokens: Vec) { #[rstest] fn snapshots_matched_date_sequence_event(date_tokens: Vec) { let captured = RefCell::new(String::new()); - let _ = determine_token_span(&date_tokens, 0); + // Observable grouping result backing the supplementary snapshot. + let (end, width) = determine_token_span(&date_tokens, 0); + assert_eq!(date_tokens[..end].join(""), "25th December 2025"); + assert_eq!(width, 18); logs_assert(|lines| { captured.replace(normalise_event_lines(lines, "matched date sequence")); (!captured.borrow().is_empty()) diff --git a/src/wrap/tokenize/parsing_tracing_snapshots.rs b/src/wrap/tokenize/parsing_tracing_snapshots.rs index 9ab33493..fbedea01 100644 --- a/src/wrap/tokenize/parsing_tracing_snapshots.rs +++ b/src/wrap/tokenize/parsing_tracing_snapshots.rs @@ -11,7 +11,11 @@ use crate::wrap::tracing_snapshot_support::normalise_event_lines; #[test] fn snapshots_footnote_reference_parsed_event() { let captured = RefCell::new(String::new()); - let _ = parse_link_or_image("[^4] tail", 0); + // Observable parse result: the footnote reference is kept atomic and the + // cursor advances past it. The snapshot is supplementary. + let (token, idx) = parse_link_or_image("[^4] tail", 0); + assert_eq!(token, "[^4]"); + assert_eq!(idx, token.len()); logs_assert(|lines| { captured.replace(normalise_event_lines(lines, "footnote reference parsed")); (!captured.borrow().is_empty()) @@ -29,7 +33,11 @@ fn snapshots_footnote_reference_parsed_event() { #[test] fn snapshots_link_or_image_parsed_event() { let captured = RefCell::new(String::new()); - let _ = parse_link_or_image("[link](url)", 0); + // Observable parse result: the whole link is kept atomic. Supplementary + // snapshot follows. + let (token, idx) = parse_link_or_image("[link](url)", 0); + assert_eq!(token, "[link](url)"); + assert_eq!(idx, token.len()); logs_assert(|lines| { captured.replace(normalise_event_lines(lines, "link or image parsed")); (!captured.borrow().is_empty()) diff --git a/src/wrap/tracing_snapshot_support.rs b/src/wrap/tracing_snapshot_support.rs index 8f834239..966951d3 100644 --- a/src/wrap/tracing_snapshot_support.rs +++ b/src/wrap/tracing_snapshot_support.rs @@ -49,3 +49,62 @@ fn stable_event_start(line: &str) -> &str { .min() .map_or(line, |index| &line[index..]) } + +#[cfg(test)] +mod proptests { + //! Property tests for the normalisation helpers. + //! + //! These prove the prefix-stripping and suffix-preserving contract holds + //! over arbitrary log lines, complementing the fixed event snapshots. + + use proptest::prelude::*; + + use super::{normalise_event_lines, stable_event_start}; + + proptest! { + /// The normalised event start is always a suffix of the input line, and + /// re-normalising it is a no-op (the level already sits at the front). + #[test] + fn stable_event_start_returns_idempotent_suffix(line in "[^\n]*") { + let start = stable_event_start(&line); + prop_assert!(line.ends_with(start)); + prop_assert_eq!(stable_event_start(start), start); + } + + /// Given a synthetic `tracing-test` line — a volatile prefix, the event + /// level, then stable content — the prefix is stripped while the level + /// and every following field survive verbatim. The prefix charset omits + /// the letters in `TRACE`/`DEBUG`, so it cannot masquerade as a level. + #[test] + fn stable_event_start_strips_prefix_and_keeps_event( + prefix in "[0-9TZ:. -]+ ", + level in prop_oneof![Just("TRACE"), Just("DEBUG")], + rest in "[^\n]*", + ) { + let stable = format!("{level} {rest}"); + let line = format!("{prefix}{stable}"); + prop_assert_eq!(stable_event_start(&line), stable.as_str()); + } + + /// `normalise_event_lines` keeps exactly the lines containing `message`, + /// leaves no trailing whitespace, and neither invents nor drops lines. + #[test] + fn normalise_event_lines_filters_and_trims( + lines in prop::collection::vec("[^\n]*", 0..8), + message in "[^\n]{1,4}", + ) { + let refs: Vec<&str> = lines.iter().map(String::as_str).collect(); + let out = normalise_event_lines(&refs, &message); + let matching = refs.iter().filter(|l| l.contains(message.as_str())).count(); + + if matching == 0 { + prop_assert!(out.is_empty()); + } else { + prop_assert_eq!(out.split('\n').count(), matching); + for segment in out.split('\n') { + prop_assert_eq!(segment, segment.trim_end()); + } + } + } + } +} From b82e59d3c7842fcd08c96811ace875c0b5cb40a4 Mon Sep 17 00:00:00 2001 From: leynos Date: Mon, 27 Jul 2026 20:03:29 +0200 Subject: [PATCH 4/5] Strengthen tracing test assertions per review Verify the declined-coupling edge case asserts the expected span ("word") and width (4) for both fixtures, retaining the footnote-exclusion check. Strengthen the `normalise_event_lines` property test to compare each output segment in order against the independently derived stable-prefix, trimmed normalisation of its matching line, keeping the existing count and trim checks. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/wrap/inline/span_helper_tracing_tests.rs | 11 ++++++---- src/wrap/tracing_snapshot_support.rs | 22 +++++++++++++++----- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/src/wrap/inline/span_helper_tracing_tests.rs b/src/wrap/inline/span_helper_tracing_tests.rs index cfdcf187..4ddb6c83 100644 --- a/src/wrap/inline/span_helper_tracing_tests.rs +++ b/src/wrap/inline/span_helper_tracing_tests.rs @@ -86,11 +86,14 @@ fn grouping_boundary_logs_declined_footnote_coupling( .iter() .map(|token| (*token).to_string()) .collect::>(); - let (end, _width) = determine_token_span(&tokens, 0); - // Observable edge-case result: a declined coupling leaves the footnote - // reference outside the returned span rather than grouping it. + let (end, width) = determine_token_span(&tokens, 0); + // Observable edge-case result: a declined coupling groups only the leading + // "word" span, leaving the footnote reference outside the returned span. + let grouped = tokens[..end].join(""); + assert_eq!(grouped, "word"); + assert_eq!(width, 4); assert!( - !tokens[..end].join("").contains("[^note]"), + !grouped.contains("[^note]"), "declined coupling must not group the footnote reference", ); assert!(logs_contain(&format!( diff --git a/src/wrap/tracing_snapshot_support.rs b/src/wrap/tracing_snapshot_support.rs index 966951d3..e304327e 100644 --- a/src/wrap/tracing_snapshot_support.rs +++ b/src/wrap/tracing_snapshot_support.rs @@ -88,6 +88,8 @@ mod proptests { /// `normalise_event_lines` keeps exactly the lines containing `message`, /// leaves no trailing whitespace, and neither invents nor drops lines. + /// Each output segment must equal the stable-prefix slice of its + /// corresponding matching line, trailing whitespace trimmed. #[test] fn normalise_event_lines_filters_and_trims( lines in prop::collection::vec("[^\n]*", 0..8), @@ -95,14 +97,24 @@ mod proptests { ) { let refs: Vec<&str> = lines.iter().map(String::as_str).collect(); let out = normalise_event_lines(&refs, &message); - let matching = refs.iter().filter(|l| l.contains(message.as_str())).count(); + // Independently derive the expected, ordered normalisation. + let expected: Vec = refs + .iter() + .filter(|line| line.contains(message.as_str())) + .map(|line| stable_event_start(line).trim_end().to_string()) + .collect(); - if matching == 0 { + if expected.is_empty() { prop_assert!(out.is_empty()); } else { - prop_assert_eq!(out.split('\n').count(), matching); - for segment in out.split('\n') { - prop_assert_eq!(segment, segment.trim_end()); + let segments: Vec<&str> = out.split('\n').collect(); + // Count check: neither invents nor drops lines. + prop_assert_eq!(segments.len(), expected.len()); + for (segment, want) in segments.iter().zip(expected.iter()) { + // Content check: the normalised segment matches in order. + prop_assert_eq!(*segment, want.as_str()); + // Trim check: no trailing whitespace survives. + prop_assert_eq!(*segment, segment.trim_end()); } } } From 4743284505b6e7f377a4938debf6ea51d42dbd93 Mon Sep 17 00:00:00 2001 From: leynos Date: Mon, 27 Jul 2026 20:07:40 +0200 Subject: [PATCH 5/5] Make tracing test assertions explicit and non-circular Assert `end == 1` explicitly in the declined-coupling case so the test can no longer pass with an empty span, alongside the existing span/width checks. Rework the `normalise_event_lines` property to pair each output segment with its matching input line: assert the segment equals the prefix-stripped, trimmed line and is a trailing slice of that line, so the property validates normalised content rather than only segment count and trimming. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/wrap/inline/span_helper_tracing_tests.rs | 6 +++++- src/wrap/tracing_snapshot_support.rs | 20 ++++++++++++-------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/wrap/inline/span_helper_tracing_tests.rs b/src/wrap/inline/span_helper_tracing_tests.rs index 4ddb6c83..1cba5aeb 100644 --- a/src/wrap/inline/span_helper_tracing_tests.rs +++ b/src/wrap/inline/span_helper_tracing_tests.rs @@ -88,7 +88,11 @@ fn grouping_boundary_logs_declined_footnote_coupling( .collect::>(); let (end, width) = determine_token_span(&tokens, 0); // Observable edge-case result: a declined coupling groups only the leading - // "word" span, leaving the footnote reference outside the returned span. + // "word" span (end == 1, never 0), leaving the footnote outside the span. + assert_eq!( + end, 1, + "declined coupling must retain the leading word span" + ); let grouped = tokens[..end].join(""); assert_eq!(grouped, "word"); assert_eq!(width, 4); diff --git a/src/wrap/tracing_snapshot_support.rs b/src/wrap/tracing_snapshot_support.rs index e304327e..4a55b8bd 100644 --- a/src/wrap/tracing_snapshot_support.rs +++ b/src/wrap/tracing_snapshot_support.rs @@ -97,22 +97,26 @@ mod proptests { ) { let refs: Vec<&str> = lines.iter().map(String::as_str).collect(); let out = normalise_event_lines(&refs, &message); - // Independently derive the expected, ordered normalisation. - let expected: Vec = refs + // The ordered subsequence of input lines that should survive. + let matched: Vec<&str> = refs .iter() + .copied() .filter(|line| line.contains(message.as_str())) - .map(|line| stable_event_start(line).trim_end().to_string()) .collect(); - if expected.is_empty() { + if matched.is_empty() { prop_assert!(out.is_empty()); } else { let segments: Vec<&str> = out.split('\n').collect(); // Count check: neither invents nor drops lines. - prop_assert_eq!(segments.len(), expected.len()); - for (segment, want) in segments.iter().zip(expected.iter()) { - // Content check: the normalised segment matches in order. - prop_assert_eq!(*segment, want.as_str()); + prop_assert_eq!(segments.len(), matched.len()); + for (segment, line) in segments.iter().zip(matched.iter()) { + // Content check: the segment is the matched line with its + // volatile prefix stripped and trailing whitespace trimmed. + prop_assert_eq!(*segment, stable_event_start(line).trim_end()); + // Provenance check (independent of the helper's internals): + // the segment is a trailing slice of its matched input line. + prop_assert!(line.trim_end().ends_with(segment)); // Trim check: no trailing whitespace survives. prop_assert_eq!(*segment, segment.trim_end()); }