Skip to content

Commit 2b6089c

Browse files
Dunqingclaude
andcommitted
feat(formatter): add snapshot-based test infrastructure
Implement a flexible snapshot-based testing framework for oxc_formatter using `insta` and build-time test generation. ## Features - **Auto-discovery**: Tests are automatically discovered from `tests/fixtures/` directory - **Individual test functions**: Each fixture file gets its own test function for easy identification - **Hierarchical options**: Support for `options.json` files at any directory level - **Multiple option sets**: Test the same input with multiple formatting configurations - **Co-located snapshots**: Snapshot files are stored next to test files (e.g., `foo.js.snap`) - **Comprehensive README**: Detailed documentation for adding and running tests ## Implementation - **build.rs**: Scans `tests/fixtures/` and generates test functions at build time - **tests/fixtures/mod.rs**: Core test infrastructure with option resolution and snapshot generation - **tests/README.md**: Complete guide for using the test framework - **Sample tests**: Example tests demonstrating JS, JSX, TS, and nested configurations 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent b2af43a commit 2b6089c

File tree

6 files changed

+477
-0
lines changed

6 files changed

+477
-0
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/oxc_formatter/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,5 +31,7 @@ rustc-hash = { workspace = true }
3131
unicode-width = "0.2"
3232

3333
[dev-dependencies]
34+
insta = { workspace = true }
3435
oxc_parser = { workspace = true }
3536
pico-args = { workspace = true }
37+
serde_json = { workspace = true }

crates/oxc_formatter/build.rs

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
use std::{
2+
env,
3+
fs::{self, File},
4+
io::Write,
5+
path::Path,
6+
};
7+
8+
fn main() {
9+
let out_dir = env::var("OUT_DIR").unwrap();
10+
let dest_path = Path::new(&out_dir).join("generated_tests.rs");
11+
let mut f = File::create(&dest_path).unwrap();
12+
13+
let fixtures_dir = Path::new("tests/fixtures");
14+
15+
if !fixtures_dir.exists() {
16+
// If no fixtures directory exists, create an empty file
17+
writeln!(f, "// No test fixtures found").unwrap();
18+
return;
19+
}
20+
21+
writeln!(f, "// Auto-generated test functions").unwrap();
22+
writeln!(f).unwrap();
23+
24+
generate_tests_for_dir(&mut f, fixtures_dir, fixtures_dir).unwrap();
25+
26+
println!("cargo:rerun-if-changed=tests/fixtures");
27+
}
28+
29+
fn generate_tests_for_dir(f: &mut File, dir: &Path, base_dir: &Path) -> std::io::Result<()> {
30+
let entries = fs::read_dir(dir)?;
31+
32+
for entry in entries {
33+
let entry = entry?;
34+
let path = entry.path();
35+
36+
if path.is_dir() {
37+
generate_tests_for_dir(f, &path, base_dir)?;
38+
} else if is_test_file(&path) {
39+
generate_test_function(f, &path, base_dir)?;
40+
}
41+
}
42+
43+
Ok(())
44+
}
45+
46+
fn is_test_file(path: &Path) -> bool {
47+
if let Some(ext) = path.extension() {
48+
matches!(ext.to_str(), Some("js" | "jsx" | "ts" | "tsx" | "mjs" | "cjs"))
49+
} else {
50+
false
51+
}
52+
}
53+
54+
fn generate_test_function(f: &mut File, file_path: &Path, base_dir: &Path) -> std::io::Result<()> {
55+
let relative_path = file_path.strip_prefix(base_dir).unwrap();
56+
let test_name = path_to_test_name(relative_path);
57+
58+
writeln!(f, "#[test]")?;
59+
writeln!(f, "fn {test_name}() {{")?;
60+
writeln!(
61+
f,
62+
" let path = std::path::Path::new(\"tests/fixtures/{}\");",
63+
relative_path.display()
64+
)?;
65+
writeln!(f, " test_file(path);")?;
66+
writeln!(f, "}}")?;
67+
writeln!(f)?;
68+
69+
Ok(())
70+
}
71+
72+
fn path_to_test_name(path: &Path) -> String {
73+
let mut name = String::new();
74+
75+
for component in path.components() {
76+
if let std::path::Component::Normal(os_str) = component {
77+
let part = os_str.to_string_lossy();
78+
if !name.is_empty() {
79+
name.push('_');
80+
}
81+
// Replace non-alphanumeric characters with underscores
82+
for c in part.chars() {
83+
if c.is_alphanumeric() {
84+
name.push(c.to_ascii_lowercase());
85+
} else {
86+
name.push('_');
87+
}
88+
}
89+
}
90+
}
91+
92+
// Remove file extension
93+
if let Some(pos) = name.rfind('_') {
94+
let after_underscore = &name[pos + 1..];
95+
if matches!(after_underscore, "js" | "jsx" | "ts" | "tsx" | "mjs" | "cjs") {
96+
name.truncate(pos);
97+
}
98+
}
99+
100+
// Ensure it starts with a letter or underscore
101+
if name.is_empty() || name.chars().next().unwrap().is_numeric() {
102+
name = format!("test_{name}");
103+
}
104+
105+
name
106+
}
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
# Oxc Formatter Tests
2+
3+
This directory contains snapshot-based tests for the oxc_formatter crate.
4+
5+
## Overview
6+
7+
The test infrastructure is designed to be simple and flexible:
8+
9+
- **File-based testing**: Just create `.js`, `.jsx`, `.ts`, or `.tsx` files in the `fixtures/` directory
10+
- **Hierarchical options**: Configure format options via `options.json` files at any directory level
11+
- **Automatic discovery**: Tests are automatically discovered via build script - no manual registration needed
12+
- **Individual test functions**: Each file gets its own test function, visible in `cargo test` output
13+
- **Snapshot testing**: Uses `insta` for snapshot testing with easy review workflow
14+
- **Co-located snapshots**: Snapshots are stored next to test files for easy navigation
15+
16+
## Directory Structure
17+
18+
```
19+
tests/
20+
├── mod.rs # Main test runner
21+
└── fixtures/ # Test input files
22+
├── js/ # JavaScript/JSX tests
23+
│ ├── options.json # Shared options for all js tests
24+
│ ├── arrow-functions.js
25+
│ ├── arrow-functions.js.snap # Snapshot file (includes extension)
26+
│ ├── simple.jsx
27+
│ ├── simple.jsx.snap
28+
│ └── nested/
29+
│ ├── options.json # Overrides parent options
30+
│ ├── example.js
31+
│ └── example.js.snap
32+
└── ts/ # TypeScript/TSX tests
33+
├── generics.ts
34+
└── generics.ts.snap
35+
```
36+
37+
## Adding New Tests
38+
39+
### Simple Test (using default options)
40+
41+
Just create a new file in `fixtures/js/` or `fixtures/ts/`:
42+
43+
```bash
44+
# Create a new test file
45+
echo "const foo = bar;" > tests/fixtures/js/my-test.js
46+
47+
# The test is automatically discovered - just run tests
48+
cargo test -p oxc_formatter --test mod
49+
```
50+
51+
The test function is automatically generated by the build script and will be named `js_my_test`. The snapshot will be created as `tests/fixtures/js/my-test.js.snap` next to your test file.
52+
53+
### Test with Custom Options
54+
55+
Create an `options.json` file in the same directory (or parent directory):
56+
57+
```json
58+
[
59+
{
60+
"semi": true,
61+
"singleQuote": false,
62+
"arrowParens": "always"
63+
},
64+
{
65+
"semi": false,
66+
"singleQuote": true,
67+
"arrowParens": "avoid",
68+
"printWidth": 120
69+
}
70+
]
71+
```
72+
73+
**Note**: Each object in the array is a separate option set. The options are displayed in the snapshot output.
74+
75+
### Nested/Organized Tests
76+
77+
Create subdirectories to organize related tests:
78+
79+
```bash
80+
mkdir -p tests/fixtures/js/classes
81+
echo "class Foo {}" > tests/fixtures/js/classes/simple.js
82+
```
83+
84+
The test will inherit options from the nearest `options.json` file in the directory tree.
85+
86+
## Supported Options
87+
88+
The following format options are supported in `options.json`:
89+
90+
- `semi`: `true` | `false` - Semicolons (maps to `Semicolons::Always` / `Semicolons::AsNeeded`)
91+
- `singleQuote`: `true` | `false` - Quote style
92+
- `jsxSingleQuote`: `true` | `false` - JSX quote style
93+
- `arrowParens`: `"always"` | `"avoid"` - Arrow function parentheses
94+
- `trailingComma`: `"none"` | `"es5"` | `"all"` - Trailing commas
95+
- `printWidth`: number - Line width
96+
- `tabWidth`: number - Indentation width
97+
- `useTabs`: `true` | `false` - Use tabs for indentation
98+
- `bracketSpacing`: `true` | `false` - Object literal spacing
99+
- `bracketSameLine`: `true` | `false` - JSX bracket on same line
100+
- `jsxBracketSameLine`: `true` | `false` - (alias for bracketSameLine)
101+
102+
## Running Tests
103+
104+
### Run all tests
105+
```bash
106+
cargo test -p oxc_formatter --test mod
107+
```
108+
109+
K
110+
### Accept new/changed snapshots
111+
```bash
112+
cargo insta test --accept -p oxc_formatter --test mod
113+
```
114+
115+
### Accept snapshots for specific tests
116+
```bash
117+
FILTER="arrow" cargo insta test --accept -p oxc_formatter --test mod
118+
```
119+
120+
### Review snapshots interactively
121+
```bash
122+
cargo insta review -p oxc_formatter
123+
```
124+
125+
## How It Works
126+
127+
1. **Build-time Discovery**: The build script (`build.rs`) scans `tests/fixtures/` for all `.{js,jsx,ts,tsx}` files
128+
2. **Code Generation**: For each file, a test function is generated (e.g., `js_arrow_functions_js()`)
129+
3. **Test Execution**: Each generated test function calls the shared `test_file()` helper
130+
4. **Filtering**: If `FILTER` env var is set, only matching paths are tested
131+
5. **Option Resolution**: For each file, walk up the directory tree to find `options.json`
132+
6. **Formatting**: Format the file with each option set
133+
7. **Snapshot**: Generate a snapshot showing input + all outputs (stored next to test file)
134+
8. **Comparison**: Compare with existing snapshot (if any)
135+
136+
## Tips
137+
138+
- **Auto-discovery**: Just add a `.js/.jsx/.ts/.tsx` file to `fixtures/` - no need to register it anywhere
139+
- **Test names**: Test function names are generated from file paths (e.g., `js/nested/example.js``js_nested_example_js`)
140+
- **Organization**: Use subdirectories to group related tests (e.g., `fixtures/js/arrows/`, `fixtures/js/classes/`)
141+
- **Shared Options**: Put `options.json` at the directory level to apply to all files in that directory
142+
- **Override Options**: Create `options.json` in a subdirectory to override parent options
143+
- **Default Options**: If no `options.json` is found, `FormatOptions::default()` is used
144+
- **File Extensions**: The source type is detected automatically from the file extension
145+
- **Co-located Snapshots**: Snapshots are stored next to test files for easy navigation and review
146+
- **Rebuilds**: The build script automatically rebuilds when files in `tests/fixtures/` change

0 commit comments

Comments
 (0)