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

Commit

Permalink
chore: test infra for transformations (#4719)
Browse files Browse the repository at this point in the history
* testing infra

* fix

* feat: better transformation

* clippy
  • Loading branch information
ematipico committed Jul 23, 2023
1 parent 9bc3630 commit 2727926
Show file tree
Hide file tree
Showing 18 changed files with 682 additions and 425 deletions.
36 changes: 27 additions & 9 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ rome_suppression = { version = "0.0.1", path = "./crates/rome_suppres
rome_text_edit = { version = "0.0.1", path = "./crates/rome_text_edit" }
rome_text_size = { version = "0.0.1", path = "./crates/rome_text_size" }
tests_macros = { path = "./crates/tests_macros" }
rome_test_utils = { path = "./crates/rome_test_utils" }

# Crates needed in the workspace
bitflags = "2.3.1"
Expand Down
4 changes: 3 additions & 1 deletion crates/rome_analyze/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ pub use crate::rule::{
RuleMeta, RuleMetadata, SuppressAction,
};
pub use crate::services::{FromServices, MissingServicesDiagnostic, ServiceBag};
pub use crate::signals::{AnalyzerAction, AnalyzerSignal, DiagnosticSignal};
pub use crate::signals::{
AnalyzerAction, AnalyzerSignal, AnalyzerTransformation, DiagnosticSignal,
};
pub use crate::syntax::{Ast, SyntaxVisitor};
pub use crate::visitor::{NodeVisitor, Visitor, VisitorContext, VisitorFinishContext};

Expand Down
5 changes: 1 addition & 4 deletions crates/rome_js_analyze/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,10 @@ smallvec = { workspace = true }
[dev-dependencies]
countme = { workspace = true, features = ["enable"] }
insta = { workspace = true, features = ["glob"] }
json_comments = "0.2.1"
rome_js_parser = { workspace = true, features = ["tests"] }
rome_json_parser = { path = "../rome_json_parser" }
rome_service = { path = "../rome_service" }
rome_text_edit = { workspace = true }
similar = "2.1.0"
tests_macros = { workspace = true }
rome_test_utils = { workspace = true }

[features]
schemars = ["dep:schemars"]
199 changes: 21 additions & 178 deletions crates/rome_js_analyze/tests/spec_tests.rs
Original file line number Diff line number Diff line change
@@ -1,42 +1,19 @@
use json_comments::StripComments;
use rome_analyze::{
AnalysisFilter, AnalyzerAction, AnalyzerOptions, ControlFlow, Never, RuleFilter,
};
use rome_console::{
fmt::{Formatter, Termcolor},
markup, Markup,
};
use rome_deserialize::json::deserialize_from_json_str;
use rome_analyze::{AnalysisFilter, AnalyzerAction, ControlFlow, Never, RuleFilter};
use rome_diagnostics::advice::CodeSuggestionAdvice;
use rome_diagnostics::termcolor::NoColor;
use rome_diagnostics::{DiagnosticExt, Error, PrintDiagnostic, Severity};
use rome_js_parser::{
parse,
test_utils::{assert_errors_are_absent, has_bogus_nodes_or_empty_slots},
JsParserOptions,
};
use rome_diagnostics::{DiagnosticExt, Severity};
use rome_js_parser::{parse, JsParserOptions};
use rome_js_syntax::{JsFileSource, JsLanguage};
use rome_service::configuration::to_analyzer_configuration;
use rome_service::settings::WorkspaceSettings;
use rome_service::Configuration;
use similar::TextDiff;
use std::{
ffi::OsStr, fmt::Write, fs::read_to_string, os::raw::c_int, path::Path, slice, sync::Once,
use rome_rowan::AstNode;
use rome_test_utils::{
assert_errors_are_absent, code_fix_to_string, create_analyzer_options, diagnostic_to_string,
has_bogus_nodes_or_empty_slots, parse_test_path, register_leak_checker, scripts_from_json,
write_analyzer_snapshot, CheckActionType,
};
use std::{ffi::OsStr, fs::read_to_string, path::Path, slice};

tests_macros::gen_tests! {"tests/specs/**/*.{cjs,js,jsx,tsx,ts,json,jsonc}", crate::run_test, "module"}
tests_macros::gen_tests! {"tests/suppression/**/*.{cjs,js,jsx,tsx,ts,json,jsonc}", crate::run_suppression_test, "module"}

fn scripts_from_json(extension: &OsStr, input_code: &str) -> Option<Vec<String>> {
if extension == "json" || extension == "jsonc" {
let input_code = StripComments::new(input_code.as_bytes());
let scripts: Vec<String> = serde_json::from_reader(input_code).ok()?;
Some(scripts)
} else {
None
}
}

fn run_test(input: &'static str, _: &str, _: &str, _: &str) {
register_leak_checker();

Expand Down Expand Up @@ -67,7 +44,7 @@ fn run_test(input: &'static str, _: &str, _: &str, _: &str) {
.unwrap_or_else(|err| panic!("failed to read {:?}: {:?}", input_file, err));
let quantity_diagnostics = if let Some(scripts) = scripts_from_json(extension, &input_code) {
for script in scripts {
write_analysis_to_snapshot(
analyze_and_snap(
&mut snapshot,
&script,
JsFileSource::js_script(),
Expand All @@ -84,7 +61,7 @@ fn run_test(input: &'static str, _: &str, _: &str, _: &str) {
let Ok(source_type) = input_file.try_into() else {
return;
};
write_analysis_to_snapshot(
analyze_and_snap(
&mut snapshot,
&input_code,
source_type,
Expand All @@ -108,19 +85,8 @@ fn run_test(input: &'static str, _: &str, _: &str, _: &str) {
}
}

enum CheckActionType {
Suppression,
Lint,
}

impl CheckActionType {
const fn is_suppression(&self) -> bool {
matches!(self, Self::Suppression)
}
}

#[allow(clippy::too_many_arguments)]
pub(crate) fn write_analysis_to_snapshot(
pub(crate) fn analyze_and_snap(
snapshot: &mut String,
input_code: &str,
source_type: JsFileSource,
Expand All @@ -135,44 +101,7 @@ pub(crate) fn write_analysis_to_snapshot(

let mut diagnostics = Vec::new();
let mut code_fixes = Vec::new();
let mut options = AnalyzerOptions::default();
// We allow a test file to configure its rule using a special
// file with the same name as the test but with extension ".options.json"
// that configures that specific rule.
let options_file = input_file.with_extension("options.json");
if let Ok(json) = std::fs::read_to_string(options_file.clone()) {
let deserialized = deserialize_from_json_str::<Configuration>(json.as_str());
if deserialized.has_errors() {
diagnostics.extend(
deserialized
.into_diagnostics()
.into_iter()
.map(|diagnostic| {
diagnostic_to_string(
options_file.file_stem().unwrap().to_str().unwrap(),
&json,
diagnostic,
)
})
.collect::<Vec<_>>(),
);
None
} else {
let configuration = deserialized.into_deserialized();
let mut settings = WorkspaceSettings::default();
settings.merge_with_configuration(configuration).unwrap();
let configuration =
to_analyzer_configuration(&settings.linter, &settings.languages, |_| vec![]);
options = AnalyzerOptions {
configuration,
..AnalyzerOptions::default()
};

Some(json)
}
} else {
None
};
let options = create_analyzer_options(input_file, &mut diagnostics);

let (_, errors) = rome_js_analyze::analyze(&root, filter, &options, source_type, |event| {
if let Some(mut diag) = event.diagnostic() {
Expand Down Expand Up @@ -236,68 +165,16 @@ pub(crate) fn write_analysis_to_snapshot(
diagnostics.push(diagnostic_to_string(file_name, input_code, error));
}

writeln!(snapshot, "# Input").unwrap();
writeln!(snapshot, "```js").unwrap();
writeln!(snapshot, "{}", input_code).unwrap();
writeln!(snapshot, "```").unwrap();
writeln!(snapshot).unwrap();

if !diagnostics.is_empty() {
writeln!(snapshot, "# Diagnostics").unwrap();
for diagnostic in &diagnostics {
writeln!(snapshot, "```").unwrap();
writeln!(snapshot, "{}", diagnostic).unwrap();
writeln!(snapshot, "```").unwrap();
writeln!(snapshot).unwrap();
}
}

if !code_fixes.is_empty() {
writeln!(snapshot, "# Actions").unwrap();
for action in code_fixes {
writeln!(snapshot, "```diff").unwrap();
writeln!(snapshot, "{}", action).unwrap();
writeln!(snapshot, "```").unwrap();
writeln!(snapshot).unwrap();
}
}
write_analyzer_snapshot(
snapshot,
input_code,
diagnostics.as_slice(),
code_fixes.as_slice(),
);

diagnostics.len()
}

/// The test runner for the analyzer is currently designed to have a
/// one-to-one mapping between test case and analyzer rules.
/// So each testing file will be run through the analyzer with only the rule
/// corresponding to the directory name. E.g., `style/useWhile/test.js`
/// will be analyzed with just the `style/useWhile` rule.
fn parse_test_path(file: &Path) -> (&str, &str) {
let rule_folder = file.parent().unwrap();
let rule_name = rule_folder.file_name().unwrap();

let group_folder = rule_folder.parent().unwrap();
let group_name = group_folder.file_name().unwrap();

(group_name.to_str().unwrap(), rule_name.to_str().unwrap())
}

fn markup_to_string(markup: Markup) -> String {
let mut buffer = Vec::new();
let mut write = Termcolor(NoColor::new(&mut buffer));
let mut fmt = Formatter::new(&mut write);
fmt.write_markup(markup).unwrap();

String::from_utf8(buffer).unwrap()
}
#[allow(clippy::let_and_return)]
fn diagnostic_to_string(name: &str, source: &str, diag: Error) -> String {
let error = diag.with_file_path(name).with_file_source_code(source);
let text = markup_to_string(markup! {
{PrintDiagnostic::verbose(&error)}
});

text
}

fn check_code_action(
path: &Path,
source: &str,
Expand Down Expand Up @@ -329,41 +206,7 @@ fn check_code_action(

// Re-parse the modified code and panic if the resulting tree has syntax errors
let re_parse = parse(&output, source_type, options);
assert_errors_are_absent(&re_parse, path);
}

fn code_fix_to_string(source: &str, action: AnalyzerAction<JsLanguage>) -> String {
let (_, text_edit) = action.mutation.as_text_edits().unwrap_or_default();

let output = text_edit.new_string(source);

let diff = TextDiff::from_lines(source, &output);

let mut diff = diff.unified_diff();
diff.context_radius(3);

diff.to_string()
}

// Check that all red / green nodes have correctly been released on exit
extern "C" fn check_leaks() {
if let Some(report) = rome_rowan::check_live() {
panic!("\n{report}")
}
}
pub(crate) fn register_leak_checker() {
// Import the atexit function from libc
extern "C" {
fn atexit(f: extern "C" fn()) -> c_int;
}

// Use an atomic Once to register the check_leaks function to be called
// when the process exits
static ONCE: Once = Once::new();
ONCE.call_once(|| unsafe {
countme::enable(true);
atexit(check_leaks);
});
assert_errors_are_absent(re_parse.tree().syntax(), re_parse.diagnostics(), path);
}

pub(crate) fn run_suppression_test(input: &'static str, _: &str, _: &str, _: &str) {
Expand All @@ -383,7 +226,7 @@ pub(crate) fn run_suppression_test(input: &'static str, _: &str, _: &str, _: &st
};

let mut snapshot = String::new();
write_analysis_to_snapshot(
analyze_and_snap(
&mut snapshot,
&input_code,
JsFileSource::jsx(),
Expand Down
Loading

0 comments on commit 2727926

Please sign in to comment.