test(formatter_tests): run conformance with CRLF/CR/BOM variants (#27779)

Fixes will land in later PR.
This commit is contained in:
leaysgur committed 2026-10-16 04:04:07 +00:00
1 parent 5605362017
commit 78b6f0260c
4 files changed
+152 -1

No files matched your search

@@ -95,3 +95,60 @@ js compatibility: 761/811 (93.83%), 23 files skipped
- js/object-prop-break-in/short-keys.js
- jsx/comments/in-attributes.js
- jsx/spread/child.js
# Input variants differ
- js/arrows/call.js (CR)
- js/binary-expressions/jsx_parent.js (CR)
- js/break-calls/react.js (CR)
- js/comments/issue-3532.js (CR)
- js/comments/issues.js (CR)
- js/comments/jsdoc.js (CR)
- js/comments/jsx.js (CR)
- js/comments/trailing_space.js (BOM)
- js/conditional/new-ternary-examples.js (CR)
- js/conditional/new-ternary-spec.js (CR)
- js/last-argument-expansion/edge_case.js (CR)
- js/last-argument-expansion/issue-7518.js (CR)
- js/last-argument-expansion/jsx.js (CR)
- js/line-suffix-boundary/boundary.js (CR)
- js/method-chain/pr-7889.js (CR)
- js/preserve-line/argument-list.js (CR)
- js/shebang/shebang-newline.js (BOM)
- js/shebang/shebang.js (BOM)
- js/ternaries/nested-in-condition.js (CR)
- js/ternaries/nested.js (CR)
- jsx/comments/in-tags.js (CR)
- jsx/comments/like-a-comment-in-jsx-text.js (CR)
- jsx/deprecated-jsx-bracket-same-line-option/jsx.js (CR)
- jsx/escape/nbsp.js (CR)
- jsx/fbt/test.js (CR)
- jsx/fragment/fragment.js (CR)
- jsx/ignore/jsx_ignore.js (CR)
- jsx/ignore/spread.js (CR)
- jsx/jsx/array-iter.js (CR)
- jsx/jsx/arrow.js (CR)
- jsx/jsx/await.js (CR)
- jsx/jsx/conditional-expression.js (CR)
- jsx/jsx/expression.js (CR)
- jsx/jsx/flow_fix_me.js (CR)
- jsx/jsx/hug.js (CR)
- jsx/jsx/logical-expression.js (CR)
- jsx/jsx/quotes.js (CR)
- jsx/jsx/return-statement.js (CR)
- jsx/jsx/spacing.js (CR)
- jsx/jsx/template-literal-in-attr.js (CR)
- jsx/last-line/last_line.js (CR)
- jsx/last-line/single_prop_multiline_string.js (CR)
- jsx/multiline-assign/test.js (CR)
- jsx/newlines/test.js (CR)
- jsx/newlines/windows.js (CR)
- jsx/optional-chaining/optional-chaining.jsx (CR)
- jsx/parentheses/argument.js (CR)
- jsx/significant-space/comments.js (CR)
- jsx/significant-space/test.js (CR)
- jsx/single-attribute-per-line/single-attribute-per-line.js (CR)
- jsx/split-attrs/test.js (CR)
- jsx/stateless-arrow-fn/test.js (CR)
- jsx/text-wrap/issue-16897.js (CR)
- jsx/text-wrap/test.js (CR)
@@ -76,3 +76,46 @@ ts compatibility: 618/660 (93.64%), 16 files skipped
- jsx/spread/child.js
- typescript/as/comments/17407.ts
- typescript/method-chain/object/issue-17239.ts
# Input variants differ
- jsx/comments/in-tags.js (CR)
- jsx/comments/like-a-comment-in-jsx-text.js (CR)
- jsx/deprecated-jsx-bracket-same-line-option/jsx.js (CR)
- jsx/escape/nbsp.js (CR)
- jsx/fbt/test.js (CR)
- jsx/fragment/fragment.js (CR)
- jsx/ignore/jsx_ignore.js (CR)
- jsx/ignore/spread.js (CR)
- jsx/jsx/array-iter.js (CR)
- jsx/jsx/arrow.js (CR)
- jsx/jsx/await.js (CR)
- jsx/jsx/conditional-expression.js (CR)
- jsx/jsx/expression.js (CR)
- jsx/jsx/flow_fix_me.js (CR)
- jsx/jsx/hug.js (CR)
- jsx/jsx/logical-expression.js (CR)
- jsx/jsx/quotes.js (CR)
- jsx/jsx/return-statement.js (CR)
- jsx/jsx/spacing.js (CR)
- jsx/jsx/template-literal-in-attr.js (CR)
- jsx/last-line/last_line.js (CR)
- jsx/last-line/single_prop_multiline_string.js (CR)
- jsx/multiline-assign/test.js (CR)
- jsx/newlines/test.js (CR)
- jsx/newlines/windows.js (CR)
- jsx/optional-chaining/optional-chaining.jsx (CR)
- jsx/parentheses/argument.js (CR)
- jsx/significant-space/comments.js (CR)
- jsx/significant-space/test.js (CR)
- jsx/single-attribute-per-line/single-attribute-per-line.js (CR)
- jsx/split-attrs/test.js (CR)
- jsx/stateless-arrow-fn/test.js (CR)
- jsx/text-wrap/issue-16897.js (CR)
- jsx/text-wrap/test.js (CR)
- typescript/comments/after-jsx-generic.tsx (CR)
- typescript/comments/jsx.tsx (CR)
- typescript/last-argument-expansion/decorated-function.tsx (CR)
- typescript/tsx/member-expression.tsx (CR)
- typescript/tsx/optional-chaining.tsx (CR)
- typescript/tsx/react.tsx (CR)
+1
View File
@@ -11,6 +11,7 @@ Test infrastructure shared by the formatter crates (e.g. `oxc_formatter`, `oxc_f
- `ensure_prettier_suite()` maintains the pinned suite at `prettier/` (gitignored), no separate clone step anywhere
- `conformance` (feature `conformance`): Prettier-conformance machinery
- Spec parsing, snapshot matching, report building
- Each input also runs as CRLF / CR / BOM variants, which must format to the same output
- Consumers add a `tests/conformance.rs` target with a `ConformanceConfig` + format callback and pin the report with `insta`;
- the crate's own `Cargo.toml` documents why the feature exists
- Option-set → typed-options mapping is shared per crate via `tests/fixtures/options.rs` (`#[path]`-included by both the fixture harness and the conformance target)
+51 -1
View File
@@ -3,6 +3,8 @@
//! Runs a language formatter over the Prettier repository's own test suite (`tests/format/<dir>`)
//! and compares the output byte-for-byte against Prettier's committed jest snapshots.
//! Every output is also re-formatted; files whose second pass differs are tracked in the report's `# Not idempotent` section.
//! Every input is also formatted with CRLF / CR line endings and with a BOM;
//! files whose variant output differs are tracked in the report's `# Input variants differ` section.
//! Language specifics arrive through [`ConformanceConfig`] (which fixture dirs, which parser names, what to ignore)
//! and the `format` callback (spec options → typed options → formatted output), so this module never depends on a language crate.
//!
@@ -122,6 +124,7 @@ where
let mut total_failed_file_count = 0;
let mut total_skipped_files = vec![];
let mut total_non_idempotent_files = vec![];
let mut total_variant_mismatch_files = vec![];
let mut failed_reports = String::new();
failed_reports.push_str("# Failed\n");
failed_reports.push('\n');
@@ -141,6 +144,7 @@ where
total_failed_file_count += results.failed.len();
total_skipped_files.extend(results.skipped);
total_non_idempotent_files.extend(results.non_idempotent);
total_variant_mismatch_files.extend(results.variant_mismatch);
for (path, (failed, passed, ratio)) in results.failed {
writeln!(
@@ -180,6 +184,12 @@ where
writeln!(report, "- {path}{note}").unwrap();
}
}
if !total_variant_mismatch_files.is_empty() {
report.push_str("\n# Input variants differ\n\n");
for (path, variants) in &total_variant_mismatch_files {
writeln!(report, "- {path} ({})", variants.join(", ")).unwrap();
}
}
Some(report)
}
@@ -260,6 +270,8 @@ struct SnapshotResults {
/// `(path, second_pass_parse_failed)` per file
/// whose re-formatted output differs from the first pass for at least one options combination
non_idempotent: Vec<(String, bool)>,
/// `(path, variant names)` per file whose input variant output differs for at least one options combination
variant_mismatch: Vec<(String, Vec<&'static str>)>,
}
/// Run the formatter and compare the output with the Prettier's snapshot.
@@ -369,6 +381,8 @@ struct CaseOutcome {
mismatch: Option<f32>,
/// Whether the second pass failed to parse, when re-formatting did not reproduce the output.
non_idempotent: Option<bool>,
/// Input variants whose output differs from the original input's.
variant_mismatch: Vec<&'static str>,
}
/// Formats one input under one option set and compares it with the snapshot's expected output.
@@ -405,6 +419,33 @@ where
outcome.non_idempotent = Some(reformatted.is_none());
}
// Input variants: line endings are normalized and a BOM is kept as is (`endOfLine` is never `auto` in the suite),
// so they must produce the same output.
// Inputs already containing `\r` or a BOM are skipped, their variants would not be equivalent.
let mut variants = vec![];
if !source_text.contains('\r') {
variants.push(("CRLF", source_text.cow_replace('\n', "\r\n").into_owned(), actual.clone()));
variants.push(("CR", source_text.cow_replace('\n', "\r").into_owned(), actual.clone()));
}
if !source_text.starts_with('\u{feff}') {
variants.push(("BOM", format!("\u{feff}{source_text}"), format!("\u{feff}{actual}")));
}
for (name, input, expected_output) in variants {
let output = format(path, &input, &call.options);
if output.as_deref() != Some(expected_output.as_str()) {
outcome.variant_mismatch.push(name);
if debug {
match &output {
Some(output) => {
println!("Input variant {name} differs ⚠️");
print_text_diff(&TextDiff::from_lines(&expected_output, output));
}
None => println!("Input variant {name} failed to parse 💥"),
}
}
}
}
let escaped =
replace_escape_and_eol(&actual, expected.contains("LF>") || expected.contains("<CR"));
@@ -453,6 +494,7 @@ struct CaseTally {
skipped: bool,
non_idempotent: bool,
second_pass_parse_failed: bool,
variant_mismatch: Vec<&'static str>,
total_diff_ratio: f32,
}
@@ -468,6 +510,11 @@ impl CaseTally {
self.non_idempotent = true;
self.second_pass_parse_failed |= second_pass_parse_failed;
}
for name in &outcome.variant_mismatch {
if !self.variant_mismatch.contains(name) {
self.variant_mismatch.push(*name);
}
}
}
/// Records the row; an input with no option set (no spec call for this language) is not counted.
@@ -489,7 +536,10 @@ impl CaseTally {
results.skipped.push(report_path.clone());
}
if self.non_idempotent {
results.non_idempotent.push((report_path, self.second_pass_parse_failed));
results.non_idempotent.push((report_path.clone(), self.second_pass_parse_failed));
}
if !self.variant_mismatch.is_empty() {
results.variant_mismatch.push((report_path, self.variant_mismatch));
}
}
}