1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
//! Shared helpers for output-oriented unit tests.
//!
//! These helpers keep theme-based `OutputPrefs` resolution and snapshot-path
//! setup consistent across multiple test modules.
use insta::Settings;
use rstest::fixture;
use std::path::PathBuf;
use crate::output_mode::OutputMode;
use crate::output_prefs::{OutputPrefs, resolve_from_theme_with};
use crate::theme::{ThemeContext, ThemePreference};
/// Environment lookup used by tests exercising optional `NO_COLOR` handling.
pub(crate) type NoColorEnv = fn(Option<String>, &str) -> Option<String>;
/// Provide a shared lookup for an optional `NO_COLOR` value.
#[fixture]
pub(crate) fn no_color_env() -> NoColorEnv {
|no_color, key| match key {
"NO_COLOR" => no_color,
_ => None,
}
}
/// Build snapshot settings rooted at `src/snapshots/<subdir>`.
pub(crate) fn snapshot_settings(subdir: &str) -> Settings {
let mut settings = Settings::new();
settings.set_snapshot_path(
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("src")
.join("snapshots")
.join(subdir),
);
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) {
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
/// version, so snapshots survive version bumps. The filter anchors on the
/// enclosing `"generator"` object and its `"name": "netsuke"` line; any other
/// `version` field in a rendered document stays visible in snapshot diffs.
/// Diagnostic-JSON snapshot tests in any module must bind through this helper
/// so the redaction is applied consistently.
pub(crate) fn diagnostic_json_snapshot_settings() -> Settings {
let mut settings = snapshot_settings("diagnostic_json");
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
}
/// Resolve explicit-theme preferences for deterministic snapshot tests.
pub(crate) fn theme_prefs(theme: ThemePreference) -> OutputPrefs {
resolve_from_theme_with(
Some(theme),
ThemeContext::new(None, None, OutputMode::Standard),
|_| 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()),
);
}
}
}