summaryrefslogtreecommitdiff
path: root/cli/tools/test
diff options
context:
space:
mode:
Diffstat (limited to 'cli/tools/test')
-rw-r--r--cli/tools/test/mod.rs98
-rw-r--r--cli/tools/test/reporters/common.rs9
-rw-r--r--cli/tools/test/reporters/dot.rs4
-rw-r--r--cli/tools/test/reporters/junit.rs353
-rw-r--r--cli/tools/test/reporters/pretty.rs3
-rw-r--r--cli/tools/test/reporters/tap.rs4
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(&current_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,