Skip to content
This repository has been archived by the owner on Aug 31, 2023. It is now read-only.

test: custom tests for formatter #2045

Merged
merged 12 commits into from
Feb 9, 2022
Merged
31 changes: 28 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,19 +198,23 @@ The command will tell the test suite to generate and update the `.rast` files.
If tests that are inside the `ok/` folder fail or if tests that are inside the `err/`
folder don't emit, the whole test suite will fail.

### Write snapshot tests for the formatter

### Write tests for the formatter

We use [insta.rs](https://insta.rs/docs) for our snapshot tests, please make sure you read its documentation to learn the basics of snapshot testing.
You should install the companion [`cargo-insta`](https://insta.rs/docs/cli/) command to assist with snapshot reviewing.

To create a new snapshot test for JavaScript, create a new file to `crates/rome_formatter/tests/specs/javascript`, e.g. `arrow.js`
To create a new snapshot test for JavaScript, create a new file to `crates/rome_formatter/tests/specs/js/`, e.g. `arrow_with_spaces.js`

```javascript
const foo = () => {
const foo = () => {
return bar
}
```

Files processed as modules must go inside the `module/` directory, files processed as script must go inside the
`script/` directory.

Run the following command to generate the new snapshot (the snapshot tests are generated by a procedure macro so we need to recompile the tests):

```bash
Expand All @@ -227,6 +231,27 @@ After test execution, you will get a new `arrow.js.snap.new` file.

To actually update the snapshot, run `cargo insta review` to interactively review and accept the pending snapshot. `arrow.js.snap.new` will be replaced with `arrow.js.snap`

Sometimes, you need to verify the formatting for different cases/options. In order to do that, create a folder with
the cases you need to verify. If we needed to follow the previous example:

1. create a folder called `arrow_with_spaces/` and move the JS file there;
2. then create a file called `options.json`
3. The content would be something like:
```json
{
"cases": [
{
"line_width": 120,
"indent_style": {"Space": 4}
}
]
}
````
4. the `cases` keyword is mandatory;
5. then each object of the array will contain the matrix of options you'd want to test.
In this case the test suite will run a **second test case** with `line_width` to 120 and `ident_style` with 4 spaces
6. when the test suite is run, you will have two outputs in your snapshot: the default one and the custom one

## VS Code Extension Development

To build the VS Code extension from source, navigate to the `editors/vscode` directory and run:
Expand Down
2 changes: 2 additions & 0 deletions Cargo.lock

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

2 changes: 2 additions & 0 deletions crates/rome_formatter/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ cfg-if = "1.0.0"
thiserror = "1.0.30"

[dev-dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tests_macros = { path = "../tests_macros" }
insta = { version = "1.10.0", features = ["glob"] }
rslint_errors = { path = "../rslint_errors" }
Expand Down
19 changes: 19 additions & 0 deletions crates/rome_formatter/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,13 @@ pub mod formatter_traits;
mod intersperse;
mod printer;
mod ts;

pub use formatter::Formatter;
use rome_rowan::TextRange;
use rome_rowan::TextSize;
use rome_rowan::TokenAtOffset;
use rslint_parser::{parse, Syntax, SyntaxError, SyntaxNode};
use std::fmt::Display;

pub use format_element::{
block_indent, concat_elements, empty_element, empty_line, group_elements, hard_line_break,
Expand Down Expand Up @@ -141,6 +143,15 @@ impl FromStr for IndentStyle {
}
}

impl Display for IndentStyle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
IndentStyle::Tab => write!(f, "Tab"),
IndentStyle::Space(size) => write!(f, "Spaces, size: {}", size),
}
}
}

#[derive(Debug, Clone, Copy)]
pub struct FormatOptions {
/// The indent style
Expand Down Expand Up @@ -168,6 +179,14 @@ impl Default for FormatOptions {
}
}

impl Display for FormatOptions {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "Indent style: {}", self.indent_style)?;
writeln!(f, "Line width: {}", self.line_width)?;
Ok(())
}
}

/// Lightweight sourcemap marker between source and output tokens
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct SourceMarker {
Expand Down
2 changes: 1 addition & 1 deletion crates/rome_formatter/tests/prettier_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ static REPORTER: DiffReport = DiffReport::new();

tests_macros::gen_tests! {"tests/specs/prettier/**/*.js", test_snapshot, "script"}

fn test_snapshot(input: &'static str, _: &str, _: &str) {
fn test_snapshot(input: &'static str, _: &str, _: &str, _: &str) {
if input.contains("typescript")
|| input.contains("jsx")
|| input.contains("flow")
Expand Down
116 changes: 109 additions & 7 deletions crates/rome_formatter/tests/spec_test.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,90 @@
use rome_core::App;
use rome_formatter::{format, FormatOptions};
use rome_formatter::{format, FormatOptions, IndentStyle};
use rome_path::RomePath;
use rslint_parser::{parse, Syntax};
use serde::{Deserialize, Serialize};
use std::fmt::Debug;
use std::fs;
use std::path::Path;
use std::path::{Path, PathBuf};

#[derive(Debug, Eq, PartialEq, Clone, Copy, Deserialize, Serialize)]
pub enum SerializableIndentStyle {
/// Tab
Tab,
/// Space, with its quantity
Space(u8),
}

impl From<SerializableIndentStyle> for IndentStyle {
fn from(test: SerializableIndentStyle) -> Self {
match test {
SerializableIndentStyle::Tab => IndentStyle::Tab,
SerializableIndentStyle::Space(spaces) => IndentStyle::Space(spaces),
}
}
}

#[derive(Debug, Deserialize, Serialize, Clone, Copy)]
pub struct SerializableFormatOptions {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "remote derive" feature of serde might be useful for this (the facade types don't have to implement From and TestOptions can deserialize to FormatOptions directly): https://serde.rs/remote-derive.html

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ematipico have you tried this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, and it's not possible with the data structure that I want to have.

Copy link
Contributor

@MichaReiser MichaReiser Feb 9, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you seen #2045 (comment)

But agree, this isn't important. I like the name SerializableFormatOptions. It makes it clear what it is about.

/// The indent style
pub indent_style: Option<SerializableIndentStyle>,

/// What's the max width of a line. Defaults to 80
pub line_width: Option<u16>,
}

impl From<SerializableFormatOptions> for FormatOptions {
fn from(test: SerializableFormatOptions) -> Self {
Self {
indent_style: test
.indent_style
.map_or_else(|| IndentStyle::Tab, |value| value.into()),
line_width: test.line_width.unwrap_or(80),
}
}
}

#[derive(Debug, Deserialize, Serialize)]
struct TestOptions {
cases: Vec<SerializableFormatOptions>,
}

#[derive(Debug, Default)]
struct SnapshotContent {
input: String,
output: Vec<(String, FormatOptions)>,
}

impl SnapshotContent {
pub fn add_output(&mut self, content: impl Into<String>, options: FormatOptions) {
self.output.push((content.into(), options))
}

pub fn set_input(&mut self, content: impl Into<String>) {
self.input = content.into();
}

pub fn snap_content(&mut self) -> String {
let mut output = String::new();
output.push_str("# Input");
output.push('\n');
output.push_str(self.input.as_str());
output.push_str("\n=============================\n");

output.push_str("# Outputs\n");
let iter = self.output.iter();
for (index, (content, options)) in iter.enumerate() {
let formal_index = index + 1;
output.push_str(format!("## Output {formal_index}\n").as_str());
output.push_str("-----\n");
output.push_str(format!("{}", options).as_str());
output.push_str("-----\n");
output.push_str(content.as_str());
}

output
}
}

/// [insta.rs](https://insta.rs/docs) snapshot testing
///
Expand All @@ -22,7 +103,7 @@ use std::path::Path;
///
/// * `json/null` -> input: `tests/specs/json/null.json`, expected output: `tests/specs/json/null.json.snap`
/// * `null` -> input: `tests/specs/null.json`, expected output: `tests/specs/null.json.snap`
pub fn run(spec_input_file: &str, _: &str, file_type: &str) {
pub fn run(spec_input_file: &str, _expected_file: &str, test_directory: &str, file_type: &str) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: The signature starts to become a bit awkward (so many strings). Could be worth having a SpecTestFile struct that has different attributes like input_file, expected_file, and file_type. I'm not sure what test_directory is, whatever it belongs to the file or is the same for all files.

let app = App::new();
let file_path = &spec_input_file;
let spec_input_file = Path::new(spec_input_file);
Expand All @@ -35,26 +116,47 @@ pub fn run(spec_input_file: &str, _: &str, file_type: &str) {

let mut rome_path = RomePath::new(file_path);
if app.can_format(&rome_path) {
let mut snapshot_content = SnapshotContent::default();
let buffer = rome_path.get_buffer_from_file();
let syntax = if file_type == "module" {
Syntax::default().module()
} else {
Syntax::default()
};

let input = fs::read_to_string(file_path).unwrap();
snapshot_content.set_input(input.as_str());

let root = parse(buffer.as_str(), 0, syntax).syntax();
let formatted_result = format(FormatOptions::default(), &root);
let file_name = spec_input_file.file_name().unwrap().to_str().unwrap();
let input = fs::read_to_string(file_path).unwrap();
let result = formatted_result.unwrap();
// we ignore the error for now
let snapshot = format!("# Input\n{}\n---\n# Output\n{}", input, result.as_code());
let result = formatted_result.unwrap();

snapshot_content.add_output(result.as_code(), FormatOptions::default());

let test_directory = PathBuf::from(test_directory);
let options_path = test_directory.join("options.json");
if options_path.exists() {
{
let mut options_path = RomePath::new(options_path.display().to_string().as_str());
// SAFETY: we checked its existence already, we assume we have rights to read it
let options: TestOptions =
serde_json::from_str(options_path.get_buffer_from_file().as_str()).unwrap();

for test_case in options.cases {
let format_options: FormatOptions = test_case.into();
let formatted_result = format(format_options, &root).unwrap();
snapshot_content.add_output(formatted_result.as_code(), format_options);
}
}
}

insta::with_settings!({
prepend_module_to_snapshot => false,
snapshot_path => spec_input_file.parent().unwrap(),
}, {
insta::assert_snapshot!(file_name, snapshot, file_name);
insta::assert_snapshot!(file_name, snapshot_content.snap_content(), file_name);
});
}
}
4 changes: 2 additions & 2 deletions crates/rome_formatter/tests/spec_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ mod formatter {

mod js_module {
use crate::spec_test;
tests_macros::gen_tests! {"tests/specs/js/module/**/**/*.js", spec_test::run, "module"}
tests_macros::gen_tests! {"tests/specs/js/module/**/*.js", spec_test::run, "module"}
}

mod js_script {
use crate::spec_test;
tests_macros::gen_tests! {"tests/specs/js/script/**/**/*.js", spec_test::run, "script"}
tests_macros::gen_tests! {"tests/specs/js/script/**/*.js", spec_test::run, "script"}
}
}
2 changes: 0 additions & 2 deletions crates/rome_formatter/tests/specs/js/import/named_import.js

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
source: crates/rome_formatter/tests/spec_test.rs
assertion_line: 157
expression: array_nested.js

---
Expand Down Expand Up @@ -46,8 +47,13 @@ let m1 = [{ a, b }, { a }, { a }];
let n1 = [{ a, b }, { a, b }];
let o1 = [{ a, b }, { a, b, c }];

---
# Output
=============================
# Outputs
## Output 1
-----
Indent style: Tab
Line width: 80
-----
let a = [[]];
let b = [[], []];
let c = [[], [], []];
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
source: crates/rome_formatter/tests/spec_test.rs
assertion_line: 43
assertion_line: 157
expression: binding_pattern.js

---
Expand All @@ -9,8 +9,13 @@ let [a,b]=c;
let [d,...e]=c;
let [aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa,bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb,...cccccccccccccccccccccccccccccc]=f;

---
# Output
=============================
# Outputs
## Output 1
-----
Indent style: Tab
Line width: 80
-----
let [a, b] = c;
let [d, ...e] = c;
let [
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
source: crates/rome_formatter/tests/spec_test.rs
assertion_line: 43
assertion_line: 157
expression: empty_lines.js

---
Expand All @@ -18,8 +18,13 @@ let a = [

];

---
# Output
=============================
# Outputs
## Output 1
-----
Indent style: Tab
Line width: 80
-----
let a = [
1,
2,
Expand Down
10 changes: 8 additions & 2 deletions crates/rome_formatter/tests/specs/js/module/array/spaces.js.snap
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
source: crates/rome_formatter/tests/spec_test.rs
assertion_line: 157
expression: spaces.js

---
Expand All @@ -10,8 +11,13 @@ let c = [,,1,];
let d = [,,1,1];
let e = [2,2,1,3];

---
# Output
=============================
# Outputs
## Output 1
-----
Indent style: Tab
Line width: 80
-----
let a = [,];
let b = [, ,];
let c = [, , 1];
Expand Down
Loading