summaryrefslogtreecommitdiff
path: root/cli/tools/test_runner.rs
diff options
context:
space:
mode:
authorCasper Beyer <caspervonb@pm.me>2021-04-29 02:17:04 +0800
committerGitHub <noreply@github.com>2021-04-28 20:17:04 +0200
commitc455c28b834683f6516422dbf1b020fbb2c1bbb6 (patch)
tree96e1484f4853969ae46539c26ffd8d716f409eb7 /cli/tools/test_runner.rs
parent0260b488fbba9a43c64641428d3603b8761067a4 (diff)
feat(test): run test modules in parallel (#9815)
This commit adds support for running test in parallel. Entire test runner functionality has been rewritten from JavaScript to Rust and a set of ops was added to support reporting in Rust. A new "--jobs" flag was added to "deno test" that allows to configure how many threads will be used. When given no value it defaults to 2.
Diffstat (limited to 'cli/tools/test_runner.rs')
-rw-r--r--cli/tools/test_runner.rs353
1 files changed, 334 insertions, 19 deletions
diff --git a/cli/tools/test_runner.rs b/cli/tools/test_runner.rs
index df792bd53..eb5b9831c 100644
--- a/cli/tools/test_runner.rs
+++ b/cli/tools/test_runner.rs
@@ -1,11 +1,56 @@
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
+use crate::colors;
+use crate::create_main_worker;
+use crate::file_fetcher::File;
+use crate::flags::Flags;
use crate::fs_util;
+use crate::media_type::MediaType;
+use crate::module_graph;
+use crate::program_state::ProgramState;
+use crate::tokio_util;
+use crate::tools::coverage::CoverageCollector;
use crate::tools::installer::is_remote_url;
use deno_core::error::AnyError;
+use deno_core::futures::future;
+use deno_core::futures::stream;
+use deno_core::futures::StreamExt;
use deno_core::serde_json::json;
use deno_core::url::Url;
+use deno_core::ModuleSpecifier;
+use deno_runtime::permissions::Permissions;
+use serde::Deserialize;
use std::path::Path;
+use std::path::PathBuf;
+use std::sync::mpsc::channel;
+use std::sync::mpsc::Sender;
+use std::sync::Arc;
+
+#[derive(Debug, Clone, PartialEq, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub enum TestResult {
+ Ok,
+ Ignored,
+ Failed(String),
+}
+
+#[derive(Debug, Clone, Deserialize)]
+#[serde(tag = "kind", content = "data", rename_all = "camelCase")]
+pub enum TestMessage {
+ Plan {
+ pending: usize,
+ filtered: usize,
+ only: bool,
+ },
+ Wait {
+ name: String,
+ },
+ Result {
+ name: String,
+ duration: usize,
+ result: TestResult,
+ },
+}
fn is_supported(p: &Path) -> bool {
use std::path::Component;
@@ -31,7 +76,7 @@ fn is_supported(p: &Path) -> bool {
}
}
-pub fn prepare_test_modules_urls(
+pub fn collect_test_module_specifiers(
include: Vec<String>,
root_path: &Path,
) -> Result<Vec<Url>, AnyError> {
@@ -63,32 +108,302 @@ pub fn prepare_test_modules_urls(
Ok(prepared)
}
-pub fn render_test_file(
- modules: Vec<Url>,
+pub async fn run_test_file(
+ program_state: Arc<ProgramState>,
+ main_module: ModuleSpecifier,
+ test_module: ModuleSpecifier,
+ permissions: Permissions,
+ channel: Sender<TestMessage>,
+) -> Result<(), AnyError> {
+ let mut worker =
+ create_main_worker(&program_state, main_module.clone(), permissions, true);
+
+ {
+ let js_runtime = &mut worker.js_runtime;
+ js_runtime
+ .op_state()
+ .borrow_mut()
+ .put::<Sender<TestMessage>>(channel.clone());
+ }
+
+ let mut maybe_coverage_collector = if let Some(ref coverage_dir) =
+ program_state.coverage_dir
+ {
+ let session = worker.create_inspector_session();
+ let coverage_dir = PathBuf::from(coverage_dir);
+ let mut coverage_collector = CoverageCollector::new(coverage_dir, session);
+ coverage_collector.start_collecting().await?;
+
+ Some(coverage_collector)
+ } else {
+ None
+ };
+
+ let execute_result = worker.execute_module(&main_module).await;
+ execute_result?;
+ worker.execute("window.dispatchEvent(new Event('load'))")?;
+
+ let execute_result = worker.execute_module(&test_module).await;
+ execute_result?;
+
+ worker.run_event_loop().await?;
+ worker.execute("window.dispatchEvent(new Event('unload'))")?;
+
+ if let Some(coverage_collector) = maybe_coverage_collector.as_mut() {
+ coverage_collector.stop_collecting().await?;
+ }
+
+ Ok(())
+}
+
+#[allow(clippy::too_many_arguments)]
+pub async fn run_tests(
+ flags: Flags,
+ include: Option<Vec<String>>,
+ no_run: bool,
fail_fast: bool,
quiet: bool,
+ allow_none: bool,
filter: Option<String>,
-) -> String {
- let mut test_file = "".to_string();
+ concurrent_jobs: usize,
+) -> Result<(), AnyError> {
+ let program_state = ProgramState::build(flags.clone()).await?;
+ let permissions = Permissions::from_options(&flags.clone().into());
+ let cwd = std::env::current_dir().expect("No current directory");
+ let include = include.unwrap_or_else(|| vec![".".to_string()]);
+ let test_modules = collect_test_module_specifiers(include, &cwd)?;
- for module in modules {
- test_file.push_str(&format!("import \"{}\";\n", module.to_string()));
+ if test_modules.is_empty() {
+ println!("No matching test modules found");
+ if !allow_none {
+ std::process::exit(1);
+ }
+ return Ok(());
}
- let options = if let Some(filter) = filter {
- json!({ "failFast": fail_fast, "reportToConsole": !quiet, "disableLog": quiet, "filter": filter })
+ let lib = if flags.unstable {
+ module_graph::TypeLib::UnstableDenoWindow
} else {
- json!({ "failFast": fail_fast, "reportToConsole": !quiet, "disableLog": quiet })
+ module_graph::TypeLib::DenoWindow
+ };
+
+ program_state
+ .prepare_module_graph(
+ test_modules.clone(),
+ lib.clone(),
+ permissions.clone(),
+ program_state.maybe_import_map.clone(),
+ )
+ .await?;
+
+ if no_run {
+ return Ok(());
+ }
+
+ // Because scripts, and therefore worker.execute cannot detect unresolved promises at the moment
+ // we generate a module for the actual test execution.
+ let test_options = json!({
+ "disableLog": quiet,
+ "filter": filter,
+ });
+
+ let test_module = deno_core::resolve_path("$deno$test.js")?;
+ let test_source =
+ format!("await Deno[Deno.internal].runTests({});", test_options);
+ let test_file = File {
+ local: test_module.to_file_path().unwrap(),
+ maybe_types: None,
+ media_type: MediaType::JavaScript,
+ source: test_source.clone(),
+ specifier: test_module.clone(),
+ };
+
+ program_state.file_fetcher.insert_cached(test_file);
+
+ let (sender, receiver) = channel::<TestMessage>();
+
+ let join_handles = test_modules.iter().map(move |main_module| {
+ let program_state = program_state.clone();
+ let main_module = main_module.clone();
+ let test_module = test_module.clone();
+ let permissions = permissions.clone();
+ let sender = sender.clone();
+
+ tokio::task::spawn_blocking(move || {
+ let join_handle = std::thread::spawn(move || {
+ let future = run_test_file(
+ program_state,
+ main_module,
+ test_module,
+ permissions,
+ sender,
+ );
+
+ tokio_util::run_basic(future)
+ });
+
+ join_handle.join().unwrap()
+ })
+ });
+
+ let join_futures = stream::iter(join_handles)
+ .buffer_unordered(concurrent_jobs)
+ .collect::<Vec<Result<Result<(), AnyError>, tokio::task::JoinError>>>();
+
+ let handler = {
+ tokio::task::spawn_blocking(move || {
+ let time = std::time::Instant::now();
+ let mut failed = 0;
+ let mut filtered_out = 0;
+ let mut ignored = 0;
+ let mut passed = 0;
+ let measured = 0;
+
+ let mut planned = 0;
+ let mut used_only = false;
+ let mut has_error = false;
+ let mut failures: Vec<(String, String)> = Vec::new();
+
+ for message in receiver.iter() {
+ match message {
+ TestMessage::Plan {
+ pending,
+ filtered,
+ only,
+ } => {
+ println!("running {} tests", pending);
+
+ if only {
+ used_only = true;
+ }
+
+ planned += pending;
+ filtered_out += filtered;
+ }
+
+ TestMessage::Wait { name } => {
+ if concurrent_jobs == 1 {
+ print!("test {} ...", name);
+ }
+ }
+
+ TestMessage::Result {
+ name,
+ duration,
+ result,
+ } => {
+ if concurrent_jobs != 1 {
+ print!("test {} ...", name);
+ }
+
+ match result {
+ TestResult::Ok => {
+ println!(
+ " {} {}",
+ colors::green("ok"),
+ colors::gray(format!("({}ms)", duration))
+ );
+
+ passed += 1;
+ }
+ TestResult::Ignored => {
+ println!(
+ " {} {}",
+ colors::yellow("ignored"),
+ colors::gray(format!("({}ms)", duration))
+ );
+
+ ignored += 1;
+ }
+ TestResult::Failed(error) => {
+ println!(
+ " {} {}",
+ colors::red("FAILED"),
+ colors::gray(format!("({}ms)", duration))
+ );
+
+ failed += 1;
+ failures.push((name, error));
+ has_error = true;
+ }
+ }
+ }
+ }
+
+ if has_error && fail_fast {
+ break;
+ }
+ }
+
+ // If one of the workers panic then we can end up with less test results than what was
+ // planned.
+ // In that case we mark it as an error so that it will be reported as failed.
+ if planned > passed + ignored + failed {
+ has_error = true;
+ }
+
+ if !failures.is_empty() {
+ println!("\nfailures:\n");
+ for (name, error) in &failures {
+ println!("{}", name);
+ println!("{}", error);
+ println!();
+ }
+
+ println!("failures:\n");
+ for (name, _) in &failures {
+ println!("\t{}", name);
+ }
+ }
+
+ let status = if has_error {
+ colors::red("FAILED").to_string()
+ } else {
+ colors::green("ok").to_string()
+ };
+
+ println!(
+ "\ntest result: {}. {} passed; {} failed; {} ignored; {} measured; {} filtered out {}\n",
+ status,
+ passed,
+ failed,
+ ignored,
+ measured,
+ filtered_out,
+ colors::gray(format!("({}ms)", time.elapsed().as_millis())),
+ );
+
+ if used_only {
+ println!(
+ "{} because the \"only\" option was used\n",
+ colors::red("FAILED")
+ );
+
+ has_error = true;
+ }
+
+ has_error
+ })
};
- test_file.push_str("// @ts-ignore\n");
+ let (result, join_results) = future::join(handler, join_futures).await;
- test_file.push_str(&format!(
- "await Deno[Deno.internal].runTests({});\n",
- options
- ));
+ let mut join_errors = join_results.into_iter().filter_map(|join_result| {
+ join_result
+ .ok()
+ .map(|handle_result| handle_result.err())
+ .flatten()
+ });
- test_file
+ if let Some(e) = join_errors.next() {
+ Err(e)
+ } else {
+ if result.unwrap_or(false) {
+ std::process::exit(1);
+ }
+
+ Ok(())
+ }
}
#[cfg(test)]
@@ -96,9 +411,9 @@ mod tests {
use super::*;
#[test]
- fn test_prepare_test_modules_urls() {
+ fn test_collect_test_module_specifiers() {
let test_data_path = test_util::root_path().join("cli/tests/subdir");
- let mut matched_urls = prepare_test_modules_urls(
+ let mut matched_urls = collect_test_module_specifiers(
vec![
"https://example.com/colors_test.ts".to_string(),
"./mod1.ts".to_string(),
@@ -156,7 +471,7 @@ mod tests {
.join("http");
println!("root {:?}", root);
let mut matched_urls =
- prepare_test_modules_urls(vec![".".to_string()], &root).unwrap();
+ collect_test_module_specifiers(vec![".".to_string()], &root).unwrap();
matched_urls.sort();
let root_url = Url::from_file_path(root).unwrap().to_string();
println!("root_url {}", root_url);