// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. use crate::ast; use crate::colors; use crate::create_main_worker; use crate::file_fetcher::File; use crate::fs_util::collect_files; use crate::fs_util::normalize_path; use crate::media_type::MediaType; use crate::module_graph; use crate::program_state::ProgramState; use crate::tokio_util; use crate::tools::coverage::CoverageCollector; use deno_core::error::AnyError; use deno_core::futures::future; use deno_core::futures::stream; use deno_core::futures::FutureExt; use deno_core::futures::StreamExt; use deno_core::located_script_name; use deno_core::serde_json::json; use deno_core::url::Url; use deno_core::ModuleSpecifier; use deno_runtime::permissions::Permissions; use regex::Regex; 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; use std::time::Instant; use swc_common::comments::CommentKind; #[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, }, } #[derive(Debug, Clone, Deserialize)] pub struct TestEvent { pub origin: String, pub message: TestMessage, } trait TestReporter { fn visit_event(&mut self, event: TestEvent); fn done(&mut self); } struct PrettyTestReporter { time: Instant, failed: usize, filtered_out: usize, ignored: usize, passed: usize, measured: usize, pending: usize, failures: Vec<(String, String)>, concurrent: bool, } impl PrettyTestReporter { fn new(concurrent: bool) -> PrettyTestReporter { PrettyTestReporter { time: Instant::now(), failed: 0, filtered_out: 0, ignored: 0, passed: 0, measured: 0, pending: 0, failures: Vec::new(), concurrent, } } } impl TestReporter for PrettyTestReporter { fn visit_event(&mut self, event: TestEvent) { match &event.message { TestMessage::Plan { pending, filtered, only: _, } => { if *pending == 1 { println!("running {} test from {}", pending, event.origin); } else { println!("running {} tests from {}", pending, event.origin); } self.pending += pending; self.filtered_out += filtered; } TestMessage::Wait { name } => { if !self.concurrent { print!("test {} ...", name); } } TestMessage::Result { name, duration, result, } => { self.pending -= 1; if self.concurrent { print!("test {} ...", name); } match result { TestResult::Ok => { println!( " {} {}", colors::green("ok"), colors::gray(format!("({}ms)", duration)) ); self.passed += 1; } TestResult::Ignored => { println!( " {} {}", colors::yellow("ignored"), colors::gray(format!("({}ms)", duration)) ); self.ignored += 1; } TestResult::Failed(error) => { println!( " {} {}", colors::red("FAILED"), colors::gray(format!("({}ms)", duration)) ); self.failed += 1; self.failures.push((name.to_string(), error.to_string())); } } } } } fn done(&mut self) { if !self.failures.is_empty() { println!("\nfailures:\n"); for (name, error) in &self.failures { println!("{}", name); println!("{}", error); println!(); } println!("failures:\n"); for (name, _) in &self.failures { println!("\t{}", name); } } let status = if self.pending > 0 || !self.failures.is_empty() { colors::red("FAILED").to_string() } else { colors::green("ok").to_string() }; println!( "\ntest result: {}. {} passed; {} failed; {} ignored; {} measured; {} filtered out {}\n", status, self.passed, self.failed, self.ignored, self.measured, self.filtered_out, colors::gray(format!("({}ms)", self.time.elapsed().as_millis())), ); } } fn create_reporter(concurrent: bool) -> Box { Box::new(PrettyTestReporter::new(concurrent)) } pub(crate) fn is_supported(p: &Path) -> bool { use std::path::Component; if let Some(Component::Normal(basename_os_str)) = p.components().next_back() { let basename = basename_os_str.to_string_lossy(); basename.ends_with("_test.ts") || basename.ends_with("_test.tsx") || basename.ends_with("_test.js") || basename.ends_with("_test.mjs") || basename.ends_with("_test.jsx") || basename.ends_with(".test.ts") || basename.ends_with(".test.tsx") || basename.ends_with(".test.js") || basename.ends_with(".test.mjs") || basename.ends_with(".test.jsx") || basename == "test.ts" || basename == "test.tsx" || basename == "test.js" || basename == "test.mjs" || basename == "test.jsx" } else { false } } pub fn is_remote_url(module_url: &str) -> bool { let lower = module_url.to_lowercase(); lower.starts_with("http://") || lower.starts_with("https://") } pub fn collect_test_module_specifiers

( include: Vec, root_path: &Path, predicate: P, ) -> Result, AnyError> where P: Fn(&Path) -> bool, { let (include_paths, include_urls): (Vec, Vec) = include.into_iter().partition(|n| !is_remote_url(n)); let mut prepared = vec![]; for path in include_paths { let p = normalize_path(&root_path.join(path)); if p.is_dir() { let test_files = collect_files(&[p], &[], &predicate).unwrap(); let test_files_as_urls = test_files .iter() .map(|f| Url::from_file_path(f).unwrap()) .collect::>(); prepared.extend(test_files_as_urls); } else { let url = Url::from_file_path(p).unwrap(); prepared.push(url); } } for remote_url in include_urls { let url = Url::parse(&remote_url)?; prepared.push(url); } Ok(prepared) } pub async fn run_test_file( program_state: Arc, main_module: ModuleSpecifier, test_module: ModuleSpecifier, permissions: Permissions, channel: Sender, ) -> 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::>(channel.clone()); } let mut maybe_coverage_collector = if let Some(ref coverage_dir) = program_state.coverage_dir { let session = worker.create_inspector_session().await; let coverage_dir = PathBuf::from(coverage_dir); let mut coverage_collector = CoverageCollector::new(coverage_dir, session); worker .with_event_loop(coverage_collector.start_collecting().boxed_local()) .await?; Some(coverage_collector) } else { None }; let execute_result = worker.execute_module(&main_module).await; execute_result?; worker.execute_script( &located_script_name!(), "window.dispatchEvent(new Event('load'))", )?; let execute_result = worker.execute_module(&test_module).await; execute_result?; worker .run_event_loop(maybe_coverage_collector.is_none()) .await?; worker.execute_script( &located_script_name!(), "window.dispatchEvent(new Event('unload'))", )?; if let Some(coverage_collector) = maybe_coverage_collector.as_mut() { worker .with_event_loop(coverage_collector.stop_collecting().boxed_local()) .await?; } Ok(()) } /// Runs tests. /// /// Returns a boolean indicating whether the tests failed. #[allow(clippy::too_many_arguments)] pub async fn run_tests( program_state: Arc, permissions: Permissions, lib: module_graph::TypeLib, doc_modules: Vec, test_modules: Vec, no_run: bool, fail_fast: bool, quiet: bool, allow_none: bool, filter: Option, concurrent_jobs: usize, ) -> Result { if !doc_modules.is_empty() { let mut test_programs = Vec::new(); let blocks_regex = Regex::new(r"```([^\n]*)\n([\S\s]*?)```")?; let lines_regex = Regex::new(r"(?:\* ?)(?:\# ?)?(.*)")?; for specifier in &doc_modules { let mut fetch_permissions = Permissions::allow_all(); let file = program_state .file_fetcher .fetch(&specifier, &mut fetch_permissions) .await?; let parsed_module = ast::parse(&file.specifier.as_str(), &file.source, &file.media_type)?; let mut comments = parsed_module.get_comments(); comments.sort_by_key(|comment| { let location = parsed_module.get_location(&comment.span); location.line }); for comment in comments { if comment.kind != CommentKind::Block || !comment.text.starts_with('*') { continue; } for block in blocks_regex.captures_iter(&comment.text) { let body = block.get(2).unwrap(); let text = body.as_str(); // TODO(caspervonb) generate an inline source map let mut source = String::new(); for line in lines_regex.captures_iter(&text) { let text = line.get(1).unwrap(); source.push_str(&format!("{}\n", text.as_str())); } source.push_str("export {};"); let element = block.get(0).unwrap(); let span = comment .span .from_inner_byte_pos(element.start(), element.end()); let location = parsed_module.get_location(&span); let specifier = deno_core::resolve_url_or_path(&format!( "{}${}-{}", location.filename, location.line, location.line + element.as_str().split('\n').count(), ))?; let file = File { local: specifier.to_file_path().unwrap(), maybe_types: None, media_type: MediaType::TypeScript, // media_type.clone(), source: source.clone(), specifier: specifier.clone(), }; program_state.file_fetcher.insert_cached(file.clone()); test_programs.push(file.specifier.clone()); } } } program_state .prepare_module_graph( test_programs.clone(), lib.clone(), Permissions::allow_all(), permissions.clone(), program_state.maybe_import_map.clone(), ) .await?; } else if test_modules.is_empty() { println!("No matching test modules found"); if !allow_none { std::process::exit(1); } return Ok(false); } program_state .prepare_module_graph( test_modules.clone(), lib.clone(), Permissions::allow_all(), permissions.clone(), program_state.maybe_import_map.clone(), ) .await?; if no_run { return Ok(false); } // 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::(); 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::, tokio::task::JoinError>>>(); let mut reporter = create_reporter(concurrent_jobs > 1); let handler = { tokio::task::spawn_blocking(move || { let mut used_only = false; let mut has_error = false; let mut planned = 0; let mut reported = 0; for event in receiver.iter() { match event.message.clone() { TestMessage::Plan { pending, filtered: _, only, } => { if only { used_only = true; } planned += pending; } TestMessage::Result { name: _, duration: _, result, } => { reported += 1; if let TestResult::Failed(_) = result { has_error = true; } } _ => {} } reporter.visit_event(event); if has_error && fail_fast { break; } } if planned > reported { has_error = true; } reporter.done(); if planned > reported { has_error = true; } if used_only { println!( "{} because the \"only\" option was used\n", colors::red("FAILED") ); has_error = true; } has_error }) }; let (result, join_results) = future::join(handler, join_futures).await; let mut join_errors = join_results.into_iter().filter_map(|join_result| { join_result .ok() .map(|handle_result| handle_result.err()) .flatten() }); if let Some(e) = join_errors.next() { Err(e) } else { Ok(result.unwrap_or(false)) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_collect_test_module_specifiers() { let test_data_path = test_util::root_path().join("cli/tests/subdir"); let mut matched_urls = collect_test_module_specifiers( vec![ "https://example.com/colors_test.ts".to_string(), "./mod1.ts".to_string(), "./mod3.js".to_string(), "subdir2/mod2.ts".to_string(), "http://example.com/printf_test.ts".to_string(), ], &test_data_path, is_supported, ) .unwrap(); let test_data_url = Url::from_file_path(test_data_path).unwrap().to_string(); let expected: Vec = vec![ format!("{}/mod1.ts", test_data_url), format!("{}/mod3.js", test_data_url), format!("{}/subdir2/mod2.ts", test_data_url), "http://example.com/printf_test.ts".to_string(), "https://example.com/colors_test.ts".to_string(), ] .into_iter() .map(|f| Url::parse(&f).unwrap()) .collect(); matched_urls.sort(); assert_eq!(matched_urls, expected); } #[test] fn test_is_supported() { assert!(is_supported(Path::new("tests/subdir/foo_test.ts"))); assert!(is_supported(Path::new("tests/subdir/foo_test.tsx"))); assert!(is_supported(Path::new("tests/subdir/foo_test.js"))); assert!(is_supported(Path::new("tests/subdir/foo_test.jsx"))); assert!(is_supported(Path::new("bar/foo.test.ts"))); assert!(is_supported(Path::new("bar/foo.test.tsx"))); assert!(is_supported(Path::new("bar/foo.test.js"))); assert!(is_supported(Path::new("bar/foo.test.jsx"))); assert!(is_supported(Path::new("foo/bar/test.js"))); assert!(is_supported(Path::new("foo/bar/test.jsx"))); assert!(is_supported(Path::new("foo/bar/test.ts"))); assert!(is_supported(Path::new("foo/bar/test.tsx"))); assert!(!is_supported(Path::new("README.md"))); assert!(!is_supported(Path::new("lib/typescript.d.ts"))); assert!(!is_supported(Path::new("notatest.js"))); assert!(!is_supported(Path::new("NotAtest.ts"))); } #[test] fn supports_dirs() { // TODO(caspervonb) generate some fixtures in a temporary directory instead, there's no need // for this to rely on external fixtures. let root = test_util::root_path() .join("test_util") .join("std") .join("http"); println!("root {:?}", root); let mut matched_urls = collect_test_module_specifiers( vec![".".to_string()], &root, is_supported, ) .unwrap(); matched_urls.sort(); let root_url = Url::from_file_path(root).unwrap().to_string(); println!("root_url {}", root_url); let expected: Vec = vec![ format!("{}/_io_test.ts", root_url), format!("{}/cookie_test.ts", root_url), format!("{}/file_server_test.ts", root_url), format!("{}/racing_server_test.ts", root_url), format!("{}/server_test.ts", root_url), format!("{}/test.ts", root_url), ] .into_iter() .map(|f| Url::parse(&f).unwrap()) .collect(); assert_eq!(matched_urls, expected); } #[test] fn test_is_remote_url() { assert!(is_remote_url("https://deno.land/std/http/file_server.ts")); assert!(is_remote_url("http://deno.land/std/http/file_server.ts")); assert!(is_remote_url("HTTP://deno.land/std/http/file_server.ts")); assert!(is_remote_url("HTTp://deno.land/std/http/file_server.ts")); assert!(!is_remote_url("file:///dev/deno_std/http/file_server.ts")); assert!(!is_remote_url("./dev/deno_std/http/file_server.ts")); } }