diff options
Diffstat (limited to 'cli/tools/test')
-rw-r--r-- | cli/tools/test/mod.rs | 98 | ||||
-rw-r--r-- | cli/tools/test/reporters/common.rs | 9 | ||||
-rw-r--r-- | cli/tools/test/reporters/dot.rs | 4 | ||||
-rw-r--r-- | cli/tools/test/reporters/junit.rs | 353 | ||||
-rw-r--r-- | cli/tools/test/reporters/pretty.rs | 3 | ||||
-rw-r--r-- | cli/tools/test/reporters/tap.rs | 4 |
6 files changed, 370 insertions, 101 deletions
diff --git a/cli/tools/test/mod.rs b/cli/tools/test/mod.rs index 68be54078..738c8c304 100644 --- a/cli/tools/test/mod.rs +++ b/cli/tools/test/mod.rs @@ -298,43 +298,77 @@ pub enum TestFailure { HasSanitizersAndOverlaps(IndexSet<String>), // Long names of overlapped tests } -impl ToString for TestFailure { - fn to_string(&self) -> String { +impl std::fmt::Display for TestFailure { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - TestFailure::JsError(js_error) => format_test_error(js_error), - TestFailure::FailedSteps(1) => "1 test step failed.".to_string(), - TestFailure::FailedSteps(n) => format!("{} test steps failed.", n), - TestFailure::IncompleteSteps => "Completed while steps were still running. Ensure all steps are awaited with `await t.step(...)`.".to_string(), - TestFailure::Incomplete => "Didn't complete before parent. Await step with `await t.step(...)`.".to_string(), + TestFailure::JsError(js_error) => { + write!(f, "{}", format_test_error(js_error)) + } + TestFailure::FailedSteps(1) => write!(f, "1 test step failed."), + TestFailure::FailedSteps(n) => write!(f, "{n} test steps failed."), + TestFailure::IncompleteSteps => { + write!(f, "Completed while steps were still running. Ensure all steps are awaited with `await t.step(...)`.") + } + TestFailure::Incomplete => { + write!( + f, + "Didn't complete before parent. Await step with `await t.step(...)`." + ) + } TestFailure::Leaked(details, trailer_notes) => { - let mut string = "Leaks detected:".to_string(); + write!(f, "Leaks detected:")?; for detail in details { - string.push_str(&format!("\n - {detail}")); + write!(f, "\n - {}", detail)?; } for trailer in trailer_notes { - string.push_str(&format!("\n{trailer}")); + write!(f, "\n{}", trailer)?; } - string + Ok(()) } TestFailure::OverlapsWithSanitizers(long_names) => { - let mut string = "Started test step while another test step with sanitizers was running:".to_string(); + write!(f, "Started test step while another test step with sanitizers was running:")?; for long_name in long_names { - string.push_str(&format!("\n * {}", long_name)); + write!(f, "\n * {}", long_name)?; } - string + Ok(()) } TestFailure::HasSanitizersAndOverlaps(long_names) => { - let mut string = "Started test step with sanitizers while another test step was running:".to_string(); + write!(f, "Started test step with sanitizers while another test step was running:")?; for long_name in long_names { - string.push_str(&format!("\n * {}", long_name)); + write!(f, "\n * {}", long_name)?; } - string + Ok(()) } } } } impl TestFailure { + pub fn overview(&self) -> String { + match self { + TestFailure::JsError(js_error) => js_error.exception_message.clone(), + TestFailure::FailedSteps(1) => "1 test step failed".to_string(), + TestFailure::FailedSteps(n) => format!("{n} test steps failed"), + TestFailure::IncompleteSteps => { + "Completed while steps were still running".to_string() + } + TestFailure::Incomplete => "Didn't complete before parent".to_string(), + TestFailure::Leaked(_, _) => "Leaks detected".to_string(), + TestFailure::OverlapsWithSanitizers(_) => { + "Started test step while another test step with sanitizers was running" + .to_string() + } + TestFailure::HasSanitizersAndOverlaps(_) => { + "Started test step with sanitizers while another test step was running" + .to_string() + } + } + } + + pub fn detail(&self) -> String { + self.to_string() + } + fn format_label(&self) -> String { match self { TestFailure::Incomplete => colors::gray("INCOMPLETE").to_string(), @@ -465,6 +499,7 @@ pub struct TestSummary { #[derive(Debug, Clone)] struct TestSpecifiersOptions { + cwd: Url, concurrent_jobs: NonZeroUsize, fail_fast: Option<NonZeroUsize>, log_level: Option<log::Level>, @@ -506,23 +541,30 @@ impl TestSummary { fn get_test_reporter(options: &TestSpecifiersOptions) -> Box<dyn TestReporter> { let parallel = options.concurrent_jobs.get() > 1; let reporter: Box<dyn TestReporter> = match &options.reporter { - TestReporterConfig::Dot => Box::new(DotTestReporter::new()), + TestReporterConfig::Dot => { + Box::new(DotTestReporter::new(options.cwd.clone())) + } TestReporterConfig::Pretty => Box::new(PrettyTestReporter::new( parallel, options.log_level != Some(Level::Error), options.filter, false, + options.cwd.clone(), )), TestReporterConfig::Junit => { - Box::new(JunitTestReporter::new("-".to_string())) + Box::new(JunitTestReporter::new(options.cwd.clone(), "-".to_string())) } TestReporterConfig::Tap => Box::new(TapTestReporter::new( + options.cwd.clone(), options.concurrent_jobs > NonZeroUsize::new(1).unwrap(), )), }; if let Some(junit_path) = &options.junit_path { - let junit = Box::new(JunitTestReporter::new(junit_path.to_string())); + let junit = Box::new(JunitTestReporter::new( + options.cwd.clone(), + junit_path.to_string(), + )); return Box::new(CompoundTestReporter::new(vec![reporter, junit])); } @@ -1641,6 +1683,14 @@ pub async fn run_tests( }) .collect(), TestSpecifiersOptions { + cwd: Url::from_directory_path(cli_options.initial_cwd()).map_err( + |_| { + generic_error(format!( + "Unable to construct URL from the path of cwd: {}", + cli_options.initial_cwd().to_string_lossy(), + )) + }, + )?, concurrent_jobs: test_options.concurrent_jobs, fail_fast: test_options.fail_fast, log_level, @@ -1780,6 +1830,14 @@ pub async fn run_tests_with_watch( }) .collect(), TestSpecifiersOptions { + cwd: Url::from_directory_path(cli_options.initial_cwd()).map_err( + |_| { + generic_error(format!( + "Unable to construct URL from the path of cwd: {}", + cli_options.initial_cwd().to_string_lossy(), + )) + }, + )?, concurrent_jobs: test_options.concurrent_jobs, fail_fast: test_options.fail_fast, log_level, diff --git a/cli/tools/test/reporters/common.rs b/cli/tools/test/reporters/common.rs index 1dc879667..e4d8d4247 100644 --- a/cli/tools/test/reporters/common.rs +++ b/cli/tools/test/reporters/common.rs @@ -136,13 +136,8 @@ pub(super) fn report_summary( if !failure.hide_in_summary() { let failure_title = format_test_for_summary(cwd, description); writeln!(writer, "{}", &failure_title).unwrap(); - writeln!( - writer, - "{}: {}", - colors::red_bold("error"), - failure.to_string() - ) - .unwrap(); + writeln!(writer, "{}: {}", colors::red_bold("error"), failure) + .unwrap(); writeln!(writer).unwrap(); failure_titles.push(failure_title); } diff --git a/cli/tools/test/reporters/dot.rs b/cli/tools/test/reporters/dot.rs index 395b5f0b6..d2e529a9c 100644 --- a/cli/tools/test/reporters/dot.rs +++ b/cli/tools/test/reporters/dot.rs @@ -12,7 +12,7 @@ pub struct DotTestReporter { } impl DotTestReporter { - pub fn new() -> DotTestReporter { + pub fn new(cwd: Url) -> DotTestReporter { let console_width = if let Some(size) = crate::util::console::console_size() { size.cols as usize @@ -23,7 +23,7 @@ impl DotTestReporter { DotTestReporter { n: 0, width: console_width, - cwd: Url::from_directory_path(std::env::current_dir().unwrap()).unwrap(), + cwd, summary: TestSummary::new(), } } diff --git a/cli/tools/test/reporters/junit.rs b/cli/tools/test/reporters/junit.rs index 464f47b8d..eea1d2aca 100644 --- a/cli/tools/test/reporters/junit.rs +++ b/cli/tools/test/reporters/junit.rs @@ -1,20 +1,29 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +use std::collections::VecDeque; use std::path::PathBuf; +use super::fmt::to_relative_path_or_remote_url; use super::*; pub struct JunitTestReporter { - path: String, + cwd: Url, + output_path: String, // Stores TestCases (i.e. Tests) by the Test ID cases: IndexMap<usize, quick_junit::TestCase>, + // Stores nodes representing test cases in such a way that can be traversed + // from child to parent to build the full test name that reflects the test + // hierarchy. + test_name_tree: TestNameTree, } impl JunitTestReporter { - pub fn new(path: String) -> Self { + pub fn new(cwd: Url, output_path: String) -> Self { Self { - path, + cwd, + output_path, cases: IndexMap::new(), + test_name_tree: TestNameTree::new(), } } @@ -24,9 +33,9 @@ impl JunitTestReporter { TestResult::Ignored => quick_junit::TestCaseStatus::skipped(), TestResult::Failed(failure) => quick_junit::TestCaseStatus::NonSuccess { kind: quick_junit::NonSuccessKind::Failure, - message: Some(failure.to_string()), + message: Some(failure.overview()), ty: None, - description: None, + description: Some(failure.detail()), reruns: vec![], }, TestResult::Cancelled => quick_junit::TestCaseStatus::NonSuccess { @@ -38,6 +47,24 @@ impl JunitTestReporter { }, } } + + fn convert_step_status( + status: &TestStepResult, + ) -> quick_junit::TestCaseStatus { + match status { + TestStepResult::Ok => quick_junit::TestCaseStatus::success(), + TestStepResult::Ignored => quick_junit::TestCaseStatus::skipped(), + TestStepResult::Failed(failure) => { + quick_junit::TestCaseStatus::NonSuccess { + kind: quick_junit::NonSuccessKind::Failure, + message: Some(failure.overview()), + ty: None, + description: Some(failure.detail()), + reruns: vec![], + } + } + } + } } impl TestReporter for JunitTestReporter { @@ -46,11 +73,10 @@ impl TestReporter for JunitTestReporter { description.name.clone(), quick_junit::TestCaseStatus::skipped(), ); - let file_name = description.location.file_name.clone(); - let file_name = file_name.strip_prefix("file://").unwrap_or(&file_name); - case - .extra - .insert(String::from("filename"), String::from(file_name)); + case.classname = Some(to_relative_path_or_remote_url( + &self.cwd, + &description.location.file_name, + )); case.extra.insert( String::from("line"), description.location.line_number.to_string(), @@ -60,6 +86,8 @@ impl TestReporter for JunitTestReporter { description.location.column_number.to_string(), ); self.cases.insert(description.id, case); + + self.test_name_tree.add_node(description.clone().into()); } fn report_plan(&mut self, _plan: &TestPlan) {} @@ -89,7 +117,29 @@ impl TestReporter for JunitTestReporter { fn report_uncaught_error(&mut self, _origin: &str, _error: Box<JsError>) {} - fn report_step_register(&mut self, _description: &TestStepDescription) {} + fn report_step_register(&mut self, description: &TestStepDescription) { + self.test_name_tree.add_node(description.clone().into()); + let test_case_name = + self.test_name_tree.construct_full_test_name(description.id); + + let mut case = quick_junit::TestCase::new( + test_case_name, + quick_junit::TestCaseStatus::skipped(), + ); + case.classname = Some(to_relative_path_or_remote_url( + &self.cwd, + &description.location.file_name, + )); + case.extra.insert( + String::from("line"), + description.location.line_number.to_string(), + ); + case.extra.insert( + String::from("col"), + description.location.column_number.to_string(), + ); + self.cases.insert(description.id, case); + } fn report_step_wait(&mut self, _description: &TestStepDescription) {} @@ -97,43 +147,13 @@ impl TestReporter for JunitTestReporter { &mut self, description: &TestStepDescription, result: &TestStepResult, - _elapsed: u64, + elapsed: u64, _tests: &IndexMap<usize, TestDescription>, - test_steps: &IndexMap<usize, TestStepDescription>, + _test_steps: &IndexMap<usize, TestStepDescription>, ) { - let status = match result { - TestStepResult::Ok => "passed", - TestStepResult::Ignored => "skipped", - TestStepResult::Failed(_) => "failure", - }; - - let root_id: usize; - let mut name = String::new(); - { - let mut ancestors = vec![]; - let mut current_desc = description; - loop { - if let Some(d) = test_steps.get(¤t_desc.parent_id) { - ancestors.push(&d.name); - current_desc = d; - } else { - root_id = current_desc.parent_id; - break; - } - } - ancestors.reverse(); - for n in ancestors { - name.push_str(n); - name.push_str(" ... "); - } - name.push_str(&description.name); - } - - if let Some(case) = self.cases.get_mut(&root_id) { - case.add_property(quick_junit::Property::new( - format!("step[{}]", status), - name, - )); + if let Some(case) = self.cases.get_mut(&description.id) { + case.status = Self::convert_step_status(result); + case.set_time(Duration::from_millis(elapsed)); } } @@ -167,44 +187,239 @@ impl TestReporter for JunitTestReporter { &mut self, elapsed: &Duration, tests: &IndexMap<usize, TestDescription>, - _test_steps: &IndexMap<usize, TestStepDescription>, + test_steps: &IndexMap<usize, TestStepDescription>, ) -> anyhow::Result<()> { let mut suites: IndexMap<String, quick_junit::TestSuite> = IndexMap::new(); for (id, case) in &self.cases { - if let Some(test) = tests.get(id) { - suites - .entry(test.location.file_name.clone()) - .and_modify(|s| { - s.add_test_case(case.clone()); - }) - .or_insert_with(|| { - quick_junit::TestSuite::new(test.location.file_name.clone()) - .add_test_case(case.clone()) - .to_owned() - }); - } + let abs_filename = match (tests.get(id), test_steps.get(id)) { + (Some(test), _) => &test.location.file_name, + (_, Some(step)) => &step.location.file_name, + (None, None) => { + unreachable!("Unknown test ID '{id}' provided"); + } + }; + + let filename = to_relative_path_or_remote_url(&self.cwd, abs_filename); + + suites + .entry(filename.clone()) + .and_modify(|s| { + s.add_test_case(case.clone()); + }) + .or_insert_with(|| { + let mut suite = quick_junit::TestSuite::new(filename); + suite.add_test_case(case.clone()); + suite + }); } let mut report = quick_junit::Report::new("deno test"); - report.set_time(*elapsed).add_test_suites( - suites - .values() - .cloned() - .collect::<Vec<quick_junit::TestSuite>>(), - ); + report + .set_time(*elapsed) + .add_test_suites(suites.into_values()); - if self.path == "-" { + if self.output_path == "-" { report .serialize(std::io::stdout()) .with_context(|| "Failed to write JUnit report to stdout")?; } else { - let file = crate::util::fs::create_file(&PathBuf::from(&self.path)) - .context("Failed to open JUnit report file.")?; + let file = + crate::util::fs::create_file(&PathBuf::from(&self.output_path)) + .context("Failed to open JUnit report file.")?; report.serialize(file).with_context(|| { - format!("Failed to write JUnit report to {}", self.path) + format!("Failed to write JUnit report to {}", self.output_path) })?; } Ok(()) } } + +#[derive(Debug, Default)] +struct TestNameTree(IndexMap<usize, TestNameTreeNode>); + +impl TestNameTree { + fn new() -> Self { + // Pre-allocate some space to avoid excessive reallocations. + Self(IndexMap::with_capacity(256)) + } + + fn add_node(&mut self, node: TestNameTreeNode) { + self.0.insert(node.id, node); + } + + /// Constructs the full test name by traversing the tree from the specified + /// node as a child to its parent nodes. + /// If the provided ID is not found in the tree, or the tree is broken (e.g. + /// a child node refers to a parent node that doesn't exist), this method + /// just panics. + fn construct_full_test_name(&self, id: usize) -> String { + let mut current_id = Some(id); + let mut name_pieces = VecDeque::new(); + + loop { + let Some(id) = current_id else { + break; + }; + + let Some(node) = self.0.get(&id) else { + // The ID specified as a parent node by the child node should exist in + // the tree, but it doesn't. In this case we give up constructing the + // full test name. + unreachable!("Unregistered test ID '{id}' provided"); + }; + + name_pieces.push_front(node.test_name.as_str()); + current_id = node.parent_id; + } + + if name_pieces.is_empty() { + unreachable!("Unregistered test ID '{id}' provided"); + } + + let v: Vec<_> = name_pieces.into(); + v.join(" > ") + } +} + +#[derive(Debug)] +struct TestNameTreeNode { + id: usize, + parent_id: Option<usize>, + test_name: String, +} + +impl From<TestDescription> for TestNameTreeNode { + fn from(description: TestDescription) -> Self { + Self { + id: description.id, + parent_id: None, + test_name: description.name, + } + } +} + +impl From<TestStepDescription> for TestNameTreeNode { + fn from(description: TestStepDescription) -> Self { + Self { + id: description.id, + parent_id: Some(description.parent_id), + test_name: description.name, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn construct_full_test_name_one_node() { + let mut tree = TestNameTree::new(); + tree.add_node(TestNameTreeNode { + id: 0, + parent_id: None, + test_name: "root".to_string(), + }); + + assert_eq!(tree.construct_full_test_name(0), "root".to_string()); + } + + #[test] + fn construct_full_test_name_two_level_hierarchy() { + let mut tree = TestNameTree::new(); + tree.add_node(TestNameTreeNode { + id: 0, + parent_id: None, + test_name: "root".to_string(), + }); + tree.add_node(TestNameTreeNode { + id: 1, + parent_id: Some(0), + test_name: "child".to_string(), + }); + + assert_eq!(tree.construct_full_test_name(0), "root".to_string()); + assert_eq!(tree.construct_full_test_name(1), "root > child".to_string()); + } + + #[test] + fn construct_full_test_name_three_level_hierarchy() { + let mut tree = TestNameTree::new(); + tree.add_node(TestNameTreeNode { + id: 0, + parent_id: None, + test_name: "root".to_string(), + }); + tree.add_node(TestNameTreeNode { + id: 1, + parent_id: Some(0), + test_name: "child".to_string(), + }); + tree.add_node(TestNameTreeNode { + id: 2, + parent_id: Some(1), + test_name: "grandchild".to_string(), + }); + + assert_eq!(tree.construct_full_test_name(0), "root".to_string()); + assert_eq!(tree.construct_full_test_name(1), "root > child".to_string()); + assert_eq!( + tree.construct_full_test_name(2), + "root > child > grandchild".to_string() + ); + } + + #[test] + fn construct_full_test_name_one_root_two_chains() { + // 0 + // / \ + // 1 2 + // / \ + // 3 4 + let mut tree = TestNameTree::new(); + tree.add_node(TestNameTreeNode { + id: 0, + parent_id: None, + test_name: "root".to_string(), + }); + tree.add_node(TestNameTreeNode { + id: 1, + parent_id: Some(0), + test_name: "child 1".to_string(), + }); + tree.add_node(TestNameTreeNode { + id: 2, + parent_id: Some(0), + test_name: "child 2".to_string(), + }); + tree.add_node(TestNameTreeNode { + id: 3, + parent_id: Some(1), + test_name: "grandchild 1".to_string(), + }); + tree.add_node(TestNameTreeNode { + id: 4, + parent_id: Some(1), + test_name: "grandchild 2".to_string(), + }); + + assert_eq!(tree.construct_full_test_name(0), "root".to_string()); + assert_eq!( + tree.construct_full_test_name(1), + "root > child 1".to_string(), + ); + assert_eq!( + tree.construct_full_test_name(2), + "root > child 2".to_string(), + ); + assert_eq!( + tree.construct_full_test_name(3), + "root > child 1 > grandchild 1".to_string(), + ); + assert_eq!( + tree.construct_full_test_name(4), + "root > child 1 > grandchild 2".to_string(), + ); + } +} diff --git a/cli/tools/test/reporters/pretty.rs b/cli/tools/test/reporters/pretty.rs index 1cd1f084f..f9121a482 100644 --- a/cli/tools/test/reporters/pretty.rs +++ b/cli/tools/test/reporters/pretty.rs @@ -28,6 +28,7 @@ impl PrettyTestReporter { echo_output: bool, filter: bool, repl: bool, + cwd: Url, ) -> PrettyTestReporter { PrettyTestReporter { parallel, @@ -37,7 +38,7 @@ impl PrettyTestReporter { filter, repl, scope_test_id: None, - cwd: Url::from_directory_path(std::env::current_dir().unwrap()).unwrap(), + cwd, did_have_user_output: false, started_tests: false, ended_tests: false, diff --git a/cli/tools/test/reporters/tap.rs b/cli/tools/test/reporters/tap.rs index 610f0bec9..0758686f0 100644 --- a/cli/tools/test/reporters/tap.rs +++ b/cli/tools/test/reporters/tap.rs @@ -23,9 +23,9 @@ pub struct TapTestReporter { } impl TapTestReporter { - pub fn new(is_concurrent: bool) -> TapTestReporter { + pub fn new(cwd: Url, is_concurrent: bool) -> TapTestReporter { TapTestReporter { - cwd: Url::from_directory_path(std::env::current_dir().unwrap()).unwrap(), + cwd, is_concurrent, header: false, planned: 0, |