Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ serde_yaml = "0.9"
proptest = "1.11.0"
googletest = "0.14.3"
pretty_assertions = "1.4.1"
regex = "1.12.2"
# Plural-selection tests must pass numeric arguments to Fluent. `ortho_config`
# exposes `LocalizationArgs` as a map of `FluentValue`, but does not re-export
# the value type, so the tests need `fluent-bundle` directly. Constrained to
Expand Down
15 changes: 15 additions & 0 deletions docs/developers-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -2074,6 +2074,21 @@ in the
`no_color_env` is shared across output-preference and theme tests that exercise
optional `NO_COLOR` lookup behaviour.

### JSON snapshot version redaction

`src/snapshot_test_support.rs` owns the snapshot settings for versioned JSON
output. Its private `add_generator_version_filter` helper is composed only by
`diagnostic_json_snapshot_settings()` and
`help_targets_json_snapshot_settings()`; individual tests must use those
specialized builders rather than adding the filter themselves. JSON diagnostic
snapshots must bind through the diagnostic builder, and JSON help-target
catalogue snapshots must bind through the help-target builder.

Text catalogue snapshots must continue to use the unfiltered
`snapshot_settings("help_targets")` builder. The filter is anchored on the
Netsuke generator object, so unrelated `version` fields remain asserted in
every snapshot.

### `test_support::fs`

`test_support::fs` (`test_support/src/fs.rs`) is the crate's single
Expand Down
46 changes: 24 additions & 22 deletions docs/snapshot-testing-in-netsuke-using-insta.md
Original file line number Diff line number Diff line change
Expand Up @@ -276,28 +276,30 @@ function, serializing locale state across the test suite.
- `test_support::set_en_localizer()` — installs `en-US` as the active locale
and returns a `LocalizerGuard`.

## Redacting the generator version in diagnostic JSON snapshots

The diagnostics JSON document embeds the generator's crate version. Left
unredacted, that field changes on every version bump and would churn every
diagnostic-JSON snapshot — this is exactly what happened on the v0.1.0-beta1
bump.

The shared `diagnostic_json_snapshot_settings()` helper in
`src/snapshot_test_support.rs` adds an insta filter that rewrites the
generator's version to `[version]`. The filter anchors on the enclosing
`"generator"` object and its `"name": "netsuke"` line, so it redacts only
the generator block's version; any other field named `version` elsewhere in
a diagnostic document remains visible in snapshot diffs.
`src/diagnostic_json_tests.rs` imports the helper.

New diagnostic-JSON snapshot tests, in any module, must bind through
`snapshot_test_support::diagnostic_json_snapshot_settings()` rather than
asserting raw output, so the redaction is applied consistently.

`schema_version` and the generator name are deliberately excluded from this
redaction: they are asserted structurally in dedicated tests, separate from
the redacted version string.
## Redacting generator versions in JSON snapshots

JSON diagnostics and help-target catalogues embed Netsuke's crate version.
Left unredacted, that field changes on every version bump and would churn their
snapshots without a behavioural change.

The shared, private `add_generator_version_filter` helper in
`src/snapshot_test_support.rs` is composed by the specialized
`diagnostic_json_snapshot_settings()` and
`help_targets_json_snapshot_settings()` builders. These builders add an insta
filter that rewrites the Netsuke generator's version to `[version]`.

JSON diagnostic snapshot tests must bind through
`snapshot_test_support::diagnostic_json_snapshot_settings()`, and JSON
help-target catalogue snapshot tests must bind through
`snapshot_test_support::help_targets_json_snapshot_settings()`. Text catalogue
tests must continue to use the unfiltered `snapshot_settings("help_targets")`
builder.

The filter anchors on the enclosing `"generator"` object and its
`"name": "netsuke"` line. It redacts only that generator version; any other
field named `version` remains visible in snapshot diffs. The generator name and
`schema_version` are deliberately excluded from this redaction and remain
asserted structurally.

## Running and Updating Snapshot Tests

Expand Down
62 changes: 38 additions & 24 deletions src/runner/help_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ use crate::ast::{NetsukeManifest, Target};
use crate::cli_localization::build_localizer;
use crate::localization::set_localizer_for_tests;
use crate::manifest;
use crate::snapshot_test_support::{snapshot_settings, theme_prefs};
use crate::snapshot_test_support::{
help_targets_json_snapshot_settings, snapshot_settings, theme_prefs,
};
use crate::theme::ThemePreference;
use anyhow::{Context, Result};
use insta::assert_snapshot;
use insta::{Settings, assert_snapshot};
use proptest::prelude::*;
use semver::Version;
use std::sync::{Arc, Mutex, TryLockError, mpsc};
Expand Down Expand Up @@ -98,16 +100,36 @@ fn send_localizer_lock_result(sender: &mpsc::SyncSender<bool>) -> Result<()> {
fn catalogue_snapshot(
locale: &str,
snapshot_name: &str,
settings: &Settings,
render: impl FnOnce(&NetsukeManifest) -> Result<String>,
) -> Result<()> {
let manifest = fixture_manifest()?;
let rendered = render_catalogue_with_locale(locale, &manifest, render)?;
snapshot_settings("help_targets").bind(|| {
settings.bind(|| {
assert_snapshot!(snapshot_name, rendered);
});
Ok(())
}

/// Render and assert a text catalogue snapshot with a selected theme.
fn text_catalogue_snapshot_with_theme(
Comment thread
leynos marked this conversation as resolved.
locale: &str,
snapshot_name: &str,
theme: ThemePreference,
) -> Result<()> {
catalogue_snapshot(
locale,
snapshot_name,
&snapshot_settings("help_targets"),
|manifest| {
Ok(normalize_fluent_isolates(&render_text(
&build_catalogue(manifest),
theme_prefs(theme),
)))
},
)
}

/// Render a catalogue while holding the localizer lock only for its global
/// localization dependency.
fn render_catalogue_with_locale(
Expand Down Expand Up @@ -150,39 +172,31 @@ fn catalogue_rendering_releases_localizer_lock_before_snapshot_work() -> Result<

#[test]
fn text_catalogue_snapshot() -> Result<()> {
catalogue_snapshot("en-US", "text_catalogue", |manifest| {
Ok(normalize_fluent_isolates(&render_text(
&build_catalogue(manifest),
theme_prefs(ThemePreference::Unicode),
)))
})
text_catalogue_snapshot_with_theme("en-US", "text_catalogue", ThemePreference::Unicode)
}

#[test]
fn accessible_catalogue_snapshot() -> Result<()> {
catalogue_snapshot("en-US", "accessible_catalogue", |manifest| {
Ok(normalize_fluent_isolates(&render_text(
&build_catalogue(manifest),
theme_prefs(ThemePreference::Ascii),
)))
})
text_catalogue_snapshot_with_theme("en-US", "accessible_catalogue", ThemePreference::Ascii)
}

#[test]
fn localized_catalogue_snapshot() -> Result<()> {
catalogue_snapshot("es-ES", "localized_catalogue_es_es", |manifest| {
Ok(normalize_fluent_isolates(&render_text(
&build_catalogue(manifest),
theme_prefs(ThemePreference::Unicode),
)))
})
text_catalogue_snapshot_with_theme(
"es-ES",
"localized_catalogue_es_es",
ThemePreference::Unicode,
)
}

#[test]
fn json_catalogue_snapshot() -> Result<()> {
catalogue_snapshot("en-US", "json_catalogue", |manifest| {
render_json(&build_catalogue(manifest))
})
catalogue_snapshot(
"en-US",
"json_catalogue",
&help_targets_json_snapshot_settings(),
|manifest| render_json(&build_catalogue(manifest)),
)
}

#[test]
Expand Down
97 changes: 93 additions & 4 deletions src/snapshot_test_support.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,24 @@ pub(crate) fn snapshot_settings(subdir: &str) -> Settings {
settings
}

/// Add a redaction filter for the Netsuke generator version.
///
/// Anchor the filter on the enclosing generator object so unrelated versioned
/// content remains visible in snapshot diffs.
fn add_generator_version_filter(settings: &mut Settings) {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
settings.add_filter(
GENERATOR_VERSION_FILTER_PATTERN,
GENERATOR_VERSION_REPLACEMENT,
);
}

/// Match the generator-version field scoped to Netsuke's generator object.
const GENERATOR_VERSION_FILTER_PATTERN: &str =
r#"("generator": \{\s*\n\s*"name": "netsuke",\s*\n\s*"version": ")[^"]+(")"#;

/// Replace the matched generator version while retaining its JSON structure.
const GENERATOR_VERSION_REPLACEMENT: &str = r"${1}[version]${2}";

/// Build snapshot settings for diagnostic-JSON documents.
///
/// Extends [`snapshot_settings`] with a redaction filter for the generator's
Expand All @@ -45,10 +63,17 @@ pub(crate) fn snapshot_settings(subdir: &str) -> Settings {
/// so the redaction is applied consistently.
pub(crate) fn diagnostic_json_snapshot_settings() -> Settings {
let mut settings = snapshot_settings("diagnostic_json");
settings.add_filter(
r#"("generator": \{\s*\n\s*"name": "netsuke",\s*\n\s*"version": ")[^"]+(")"#,
r"${1}[version]${2}",
);
add_generator_version_filter(&mut settings);
settings
}

/// Build snapshot settings for JSON help-target catalogues.
///
/// Extend the help-target settings with the generator-version redaction while
/// retaining unfiltered settings for text catalogues.
pub(crate) fn help_targets_json_snapshot_settings() -> Settings {
let mut settings = snapshot_settings("help_targets");
add_generator_version_filter(&mut settings);
settings
}

Expand All @@ -60,3 +85,67 @@ pub(crate) fn theme_prefs(theme: ThemePreference) -> OutputPrefs {
|_| None,
)
}

#[cfg(test)]
mod tests {
//! Verify that generator-version redaction remains scoped across `SemVer` values.

use super::*;
use proptest::prelude::*;
use regex::Regex;
use serde_json::Value;

proptest! {
#[test]
fn generator_version_filter_redacts_semver_variants(
major in any::<u64>(),
minor in any::<u64>(),
patch in any::<u64>(),
prerelease in prop_oneof![
Just(String::new()),
"[A-Za-z][A-Za-z0-9-]{0,31}".prop_map(|identifier| format!("-{identifier}")),
],
build in prop_oneof![
Just(String::new()),
"[A-Za-z][A-Za-z0-9-]{0,31}".prop_map(|identifier| format!("+{identifier}")),
],
unrelated_version in "[A-Za-z0-9.+-]{1,64}",
) {
let generator_version = format!("{major}.{minor}.{patch}{prerelease}{build}");
let rendered = format!(
concat!(
"{{\n",
" \"generator\": {{\n",
" \"name\": \"netsuke\",\n",
" \"version\": \"{}\"\n",
" }},\n",
" \"tool\": {{\n",
" \"name\": \"netsuke\",\n",
" \"version\": \"{}\"\n",
" }}\n",
"}}",
),
generator_version,
unrelated_version,
);
let filter = match Regex::new(GENERATOR_VERSION_FILTER_PATTERN) {
Ok(filter) => filter,
Err(error) => return Err(TestCaseError::fail(error.to_string())),
};
let filtered = filter.replace_all(&rendered, GENERATOR_VERSION_REPLACEMENT);
let document = match serde_json::from_str::<Value>(&filtered) {
Ok(document) => document,
Err(error) => return Err(TestCaseError::fail(error.to_string())),
};

prop_assert_eq!(
document.pointer("/generator/version").and_then(Value::as_str),
Some("[version]"),
);
prop_assert_eq!(
document.pointer("/tool/version").and_then(Value::as_str),
Some(unrelated_version.as_str()),
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ expression: rendered
"schema_version": 1,
"generator": {
"name": "netsuke",
"version": "0.1.0-beta2"
"version": "[version]"
},
"result": {
"command": "help-targets",
Expand Down
Loading