Skip to content

Commit f6ebe33

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 f6ebe33

File tree

6 files changed

+481
-0
lines changed

6 files changed

+481
-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: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,5 +31,10 @@ 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 }
38+
39+
[build-dependencies]
40+
oxc_span = { workspace = true }

crates/oxc_formatter/build.rs

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
use std::{
2+
env,
3+
fs::{self, File},
4+
io::Write,
5+
path::Path,
6+
};
7+
8+
use oxc_span::SourceType;
9+
10+
fn main() {
11+
let out_dir = env::var("OUT_DIR").unwrap();
12+
let dest_path = Path::new(&out_dir).join("generated_tests.rs");
13+
let mut f = File::create(&dest_path).unwrap();
14+
15+
let fixtures_dir = Path::new("tests/fixtures");
16+
17+
if !fixtures_dir.exists() {
18+
// If no fixtures directory exists, create an empty file
19+
writeln!(f, "// No test fixtures found").unwrap();
20+
return;
21+
}
22+
23+
writeln!(f, "// Auto-generated test functions").unwrap();
24+
writeln!(f).unwrap();
25+
26+
generate_tests_for_dir(&mut f, fixtures_dir, fixtures_dir).unwrap();
27+
28+
println!("cargo:rerun-if-changed=tests/fixtures");
29+
}
30+
31+
fn generate_tests_for_dir(f: &mut File, dir: &Path, base_dir: &Path) -> std::io::Result<()> {
32+
let entries = fs::read_dir(dir)?;
33+
34+
for entry in entries {
35+
let entry = entry?;
36+
let path = entry.path();
37+
38+
if path.is_dir() {
39+
generate_tests_for_dir(f, &path, base_dir)?;
40+
} else if is_test_file(&path) {
41+
generate_test_function(f, &path, base_dir)?;
42+
}
43+
}
44+
45+
Ok(())
46+
}
47+
48+
fn is_test_file(path: &Path) -> bool {
49+
if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
50+
SourceType::from_extension(ext).is_ok()
51+
} else {
52+
false
53+
}
54+
}
55+
56+
fn generate_test_function(f: &mut File, file_path: &Path, base_dir: &Path) -> std::io::Result<()> {
57+
let relative_path = file_path.strip_prefix(base_dir).unwrap();
58+
let test_name = path_to_test_name(relative_path);
59+
60+
writeln!(f, "#[test]")?;
61+
writeln!(f, "fn {test_name}() {{")?;
62+
writeln!(
63+
f,
64+
" let path = std::path::Path::new(\"tests/fixtures/{}\");",
65+
relative_path.display()
66+
)?;
67+
writeln!(f, " test_file(path);")?;
68+
writeln!(f, "}}")?;
69+
writeln!(f)?;
70+
71+
Ok(())
72+
}
73+
74+
fn path_to_test_name(path: &Path) -> String {
75+
let mut name = String::new();
76+
77+
for component in path.components() {
78+
if let std::path::Component::Normal(os_str) = component {
79+
let part = os_str.to_string_lossy();
80+
if !name.is_empty() {
81+
name.push('_');
82+
}
83+
// Replace non-alphanumeric characters with underscores
84+
for c in part.chars() {
85+
if c.is_alphanumeric() {
86+
name.push(c.to_ascii_lowercase());
87+
} else {
88+
name.push('_');
89+
}
90+
}
91+
}
92+
}
93+
94+
// Remove file extension
95+
if let Some(pos) = name.rfind('_') {
96+
let after_underscore = &name[pos + 1..];
97+
if SourceType::from_extension(after_underscore).is_ok() {
98+
name.truncate(pos);
99+
}
100+
}
101+
102+
// Ensure it starts with a letter or underscore
103+
if name.is_empty() || name.chars().next().unwrap().is_numeric() {
104+
name = format!("test_{name}");
105+
}
106+
107+
name
108+
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
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+
### Accept new/changed snapshots
110+
```bash
111+
cargo insta test --accept -p oxc_formatter --test mod
112+
```
113+
114+
### Accept snapshots for specific tests
115+
```bash
116+
FILTER="arrow" cargo insta test --accept -p oxc_formatter --test mod
117+
```
118+
119+
### Review snapshots interactively
120+
```bash
121+
cargo insta review -p oxc_formatter
122+
```
123+
124+
## How It Works
125+
126+
1. **Build-time Discovery**: The build script (`build.rs`) scans `tests/fixtures/` for all `.{js,jsx,ts,tsx}` files
127+
2. **Code Generation**: For each file, a test function is generated (e.g., `js_arrow_functions_js()`)
128+
3. **Test Execution**: Each generated test function calls the shared `test_file()` helper
129+
4. **Filtering**: If `FILTER` env var is set, only matching paths are tested
130+
5. **Option Resolution**: For each file, walk up the directory tree to find `options.json`
131+
6. **Formatting**: Format the file with each option set
132+
7. **Snapshot**: Generate a snapshot showing input + all outputs (stored next to test file)
133+
8. **Comparison**: Compare with existing snapshot (if any)
134+
135+
## Tips
136+
137+
- **Auto-discovery**: Just add a `.js/.jsx/.ts/.tsx` file to `fixtures/` - no need to register it anywhere
138+
- **Test names**: Test function names are generated from file paths (e.g., `js/nested/example.js``js_nested_example_js`)
139+
- **Organization**: Use subdirectories to group related tests (e.g., `fixtures/js/arrows/`, `fixtures/js/classes/`)
140+
- **Shared Options**: Put `options.json` at the directory level to apply to all files in that directory
141+
- **Override Options**: Create `options.json` in a subdirectory to override parent options
142+
- **Default Options**: If no `options.json` is found, `FormatOptions::default()` is used
143+
- **File Extensions**: The source type is detected automatically from the file extension
144+
- **Co-located Snapshots**: Snapshots are stored next to test files for easy navigation and review
145+
- **Rebuilds**: The build script automatically rebuilds when files in `tests/fixtures/` change

0 commit comments

Comments
 (0)