/
githubmirror
/
deno
Обзор
Документация
Войти
/
githubmirror
/
deno
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
tests/specs/mod.rs
823 строки
24 KB
em
fix(runtime): capture blob worker roots before revocation (#35128)
24 июн 2026, 09:10
Не верифицирован
24 июн 2026, 09:10
118b787
Код
Авторство
О чём код?
// Copyright 2018-2026 the Deno authors. MIT license. use std::borrow::Cow; use std::cell::RefCell; use std::collections::BTreeMap; use std::collections::HashMap; use std::collections::HashSet; use std::panic::AssertUnwindSafe; use std::path::Path; use std::rc::Rc; use std::sync::Arc; use anyhow::Context; use file_test_runner::NO_CAPTURE; use file_test_runner::TestResult; use file_test_runner::collection::CollectOptions; use file_test_runner::collection::CollectTestsError; use file_test_runner::collection::CollectedCategoryOrTest; use file_test_runner::collection::CollectedTest; use file_test_runner::collection::CollectedTestCategory; use file_test_runner::collection::collect_tests_or_exit; use file_test_runner::collection::strategies::FileTestMapperStrategy; use file_test_runner::collection::strategies::TestPerDirectoryCollectionStrategy; use serde::Deserialize; use test_util::IS_CI; use test_util::PathRef; use test_util::TestContextBuilder; use test_util::test_runner::FlakyTestTracker; use test_util::test_runner::Parallelism; use test_util::test_runner::run_maybe_flaky_test; use test_util::tests_path; const MANIFEST_FILE_NAME: &str = "__test__.jsonc"; #[derive(Clone, Deserialize)] #[serde(untagged)] enum VecOrString { Vec(Vec<String>), String(String), } type JsonMap = serde_json::Map<String, serde_json::Value>; #[derive(Clone, Deserialize)] #[serde(deny_unknown_fields, rename_all = "camelCase")] struct MultiTestMetaData { /// Whether to copy all the non-assertion files in the current /// test directory to a temporary directory before running the /// steps. #[serde(default)] pub temp_dir: bool, /// The base environment to use for the test. #[serde(default)] pub base: Option<String>, #[serde(default)] pub envs: HashMap<String, String>, #[serde(default)] pub cwd: Option<String>, #[serde(default)] pub tests: BTreeMap<String, JsonMap>, #[serde(default)] pub ignore: bool, #[serde(default)] pub variants: BTreeMap<String, JsonMap>, /// Timeout in seconds for each step. Defaults to 300 (5 minutes). #[serde(default)] pub timeout: Option<u64>, } impl MultiTestMetaData { pub fn into_collected_tests( mut self, parent_test: &CollectedTest, ) -> Vec<CollectedCategoryOrTest<serde_json::Value>> { fn merge_json_value( multi_test_meta_data: &MultiTestMetaData, value: &mut JsonMap, ) { if let Some(base) = &multi_test_meta_data.base && !value.contains_key("base") { value.insert("base".to_string(), base.clone().into()); } if multi_test_meta_data.temp_dir && !value.contains_key("tempDir") { value.insert("tempDir".to_string(), true.into()); } if multi_test_meta_data.cwd.is_some() && !value.contains_key("cwd") { value .insert("cwd".to_string(), multi_test_meta_data.cwd.clone().into()); } if !multi_test_meta_data.envs.is_empty() { if !value.contains_key("envs") { value.insert("envs".to_string(), JsonMap::default().into()); } let envs_obj = value.get_mut("envs").unwrap().as_object_mut().unwrap(); for (key, value) in &multi_test_meta_data.envs { if !envs_obj.contains_key(key) { envs_obj.insert(key.into(), value.clone().into()); } } } if let Some(timeout) = multi_test_meta_data.timeout && !value.contains_key("timeout") { value.insert("timeout".to_string(), timeout.into()); } if multi_test_meta_data.ignore && !value.contains_key("ignore") { value.insert("ignore".to_string(), true.into()); } if !multi_test_meta_data.variants.is_empty() { if !value.contains_key("variants") { value.insert("variants".to_string(), JsonMap::default().into()); } let variants_obj = value.get_mut("variants").unwrap().as_object_mut().unwrap(); for (key, value) in &multi_test_meta_data.variants { if !variants_obj.contains_key(key) { variants_obj.insert(key.into(), value.clone().into()); } } } } let mut collected_tests = Vec::with_capacity(self.tests.len()); for (name, mut json_data) in std::mem::take(&mut self.tests) { merge_json_value(&self, &mut json_data); collected_tests.push(CollectedTest { name: format!("{}::{}", parent_test.name, name), path: parent_test.path.clone(), line_and_column: None, data: serde_json::Value::Object(json_data), }); } let mut all_tests = Vec::with_capacity(collected_tests.len()); for test in collected_tests { if let Some(variants) = test .data .as_object() .and_then(|o| o.get("variants")) .and_then(|v| v.as_object()) && !variants.is_empty() { all_tests.push( map_variants(&test.data, &test.path, &test.name, variants.iter()) .unwrap(), ); } else { all_tests.push(CollectedCategoryOrTest::Test(test)); } } all_tests } } #[derive(Clone, Deserialize)] #[serde(deny_unknown_fields, rename_all = "camelCase")] struct MultiStepMetaData { /// Whether to copy all the non-assertion files in the current /// test directory to a temporary directory before running the /// steps. #[serde(default)] pub temp_dir: bool, /// Whether to run this test. #[serde(rename = "if")] pub if_cond: Option<String>, /// Whether the temporary directory should be canonicalized. /// /// This should be used sparingly, but is sometimes necessary /// on the CI. #[serde(default)] pub canonicalized_temp_dir: bool, /// Whether the temporary directory should be symlinked to another path. #[serde(default)] pub symlinked_temp_dir: bool, #[serde(default)] pub flaky: bool, /// The base environment to use for the test. #[serde(default)] pub base: Option<String>, #[serde(default)] pub cwd: Option<String>, #[serde(default)] pub envs: HashMap<String, String>, #[serde(default)] pub repeat: Option<usize>, #[serde(default)] pub steps: Vec<StepMetaData>, #[serde(default)] pub ignore: bool, /// Timeout in seconds for each step. Defaults to 300 (5 minutes). #[serde(default)] pub timeout: Option<u64>, #[serde(default)] pub variants: BTreeMap<String, JsonMap>, } #[derive(Clone, Deserialize)] #[serde(deny_unknown_fields, rename_all = "camelCase")] struct SingleTestMetaData { #[serde(default)] pub base: Option<String>, #[serde(default)] pub temp_dir: bool, #[serde(default)] pub canonicalized_temp_dir: bool, #[serde(default)] pub symlinked_temp_dir: bool, #[serde(default)] pub repeat: Option<usize>, #[serde(flatten)] pub step: StepMetaData, #[serde(default)] pub ignore: bool, #[allow( dead_code, reason = "deserialized but used only in multi-step conversion" )] #[serde(default)] pub variants: BTreeMap<String, JsonMap>, /// Timeout in seconds for each step. Defaults to 300 (5 minutes). #[serde(default)] pub timeout: Option<u64>, } impl SingleTestMetaData { pub fn into_multi(self) -> MultiStepMetaData { MultiStepMetaData { base: self.base, cwd: None, if_cond: self.step.if_cond.clone(), flaky: self.step.flaky, temp_dir: self.temp_dir, canonicalized_temp_dir: self.canonicalized_temp_dir, symlinked_temp_dir: self.symlinked_temp_dir, repeat: self.repeat, envs: Default::default(), steps: vec![self.step], ignore: self.ignore, variants: self.variants, timeout: self.timeout, } } } #[derive(Clone, Deserialize)] #[serde(deny_unknown_fields, rename_all = "camelCase")] struct StepMetaData { /// If the test should be retried multiple times on failure. #[serde(default)] pub flaky: bool, pub args: VecOrString, pub cwd: Option<String>, #[serde(rename = "if")] pub if_cond: Option<String>, pub command_name: Option<String>, #[serde(default)] pub envs: HashMap<String, String>, pub input: Option<String>, pub output: String, #[serde(default)] pub exit_code: i32, #[serde(default)] pub variants: BTreeMap<String, JsonMap>, } pub fn main() { // When running from Claude Code, require a test filter (additional arg) // to prevent accidentally running the entire spec test suite. if std::env::var_os("CLAUDE_CODE_ENTRYPOINT").is_some() { let has_arg = std::env::args().skip(1).len() > 0; if !has_arg { panic!( "Running the entire spec test suite from Claude Code is not allowed. \ Please provide a test filter, e.g.: ./x test-spec my_test_name" ); } } let ci_hash = test_util::hash::check_ci_hash("specs", |hasher| { let tests = test_util::tests_path(); hasher .hash_dir(tests.join("specs")) .hash_dir(tests.join("util")) .hash_dir(tests.join("testdata")) .hash_dir(tests.join("registry")) .hash_file(test_util::deno_exe_path()) .hash_file(test_util::test_server_path()) .hash_file(test_util::denort_exe_path()); }); if matches!(ci_hash, test_util::hash::CiHashStatus::Skip) { return; } let root_category = collect_tests_or_exit::<serde_json::Value>(CollectOptions { base: tests_path().join("specs").to_path_buf(), strategy: Box::new(FileTestMapperStrategy { base_strategy: TestPerDirectoryCollectionStrategy { file_name: MANIFEST_FILE_NAME.to_string(), }, map: map_test_within_file, }), filter_override: None, }) .into_flat_category(); let root_category = if let Some(shard) = test_util::test_runner::ShardConfig::from_env() { test_util::test_runner::filter_to_shard(root_category, &shard) } else { root_category }; if root_category.is_empty() { return; // all tests filtered out } if test_util::test_runner::print_tests_if_list_flag(&root_category) { return; } let _http_guard = test_util::http_server(); let parallelism = Parallelism::default(); let flaky_test_tracker = Arc::new(FlakyTestTracker::default()); file_test_runner::run_tests( &root_category, file_test_runner::RunOptions { parallelism: parallelism.max_parallelism(), reporter: test_util::test_runner::get_test_reporter( "specs", flaky_test_tracker.clone(), ), }, move |test| run_test(test, &flaky_test_tracker, ¶llelism), ); if let test_util::hash::CiHashStatus::RunThenCommit(pending) = ci_hash { pending.commit(); } } fn run_test( test: &CollectedTest<serde_json::Value>, flaky_test_tracker: &FlakyTestTracker, parallelism: &Parallelism, ) -> TestResult { let cwd = PathRef::new(&test.path).parent(); let metadata_value = test.data.clone(); let diagnostic_logger = Rc::new(RefCell::new(Vec::<u8>::new())); let result = TestResult::from_maybe_panic_or_result(AssertUnwindSafe(|| { let metadata = deserialize_value(metadata_value); let substs = variant_substitutions(&BTreeMap::new(), &metadata.variants); let if_cond = metadata .if_cond .as_deref() .map(|s| apply_substs(s, &substs)); if metadata.ignore || !should_run(if_cond.as_deref()) { TestResult::Ignored } else if let Some(repeat) = metadata.repeat { for _ in 0..repeat { let result = run_test_inner( test, &metadata, &cwd, diagnostic_logger.clone(), flaky_test_tracker, parallelism, ); if result.is_failed() { return result; } } TestResult::Passed { duration: None } } else { run_test_inner( test, &metadata, &cwd, diagnostic_logger.clone(), flaky_test_tracker, parallelism, ) } })); match result { TestResult::Failed { duration, output: panic_output, } => { let mut output = diagnostic_logger.borrow().clone(); output.push(b'\n'); output.extend(panic_output); TestResult::Failed { duration, output } } TestResult::Passed { .. } | TestResult::Ignored | TestResult::SubTests { .. } => result, } } fn run_test_inner( test: &CollectedTest<serde_json::Value>, metadata: &MultiStepMetaData, cwd: &PathRef, diagnostic_logger: Rc<RefCell<Vec<u8>>>, flaky_test_tracker: &FlakyTestTracker, parallelism: &Parallelism, ) -> TestResult { let run_fn = || { let context = test_context_from_metadata(metadata, cwd, diagnostic_logger.clone()); for step in metadata .steps .iter() .filter(|s| should_run(s.if_cond.as_deref())) { let run_func = || { TestResult::from_maybe_panic_or_result(AssertUnwindSafe(|| { run_step(step, metadata, cwd, &context); TestResult::Passed { duration: None } })) }; let result = run_maybe_flaky_test( &test.name, step.flaky, flaky_test_tracker, None, run_func, ); if result.is_failed() { return result; } } TestResult::Passed { duration: None } }; run_maybe_flaky_test( &test.name, metadata.flaky || *IS_CI, flaky_test_tracker, Some(parallelism), run_fn, ) } fn deserialize_value(metadata_value: serde_json::Value) -> MultiStepMetaData { let metadata_string = metadata_value.to_string(); // checking for "steps" leads to a more targeted error message // instead of when deserializing an untagged enum if metadata_value .as_object() .map(|o| o.contains_key("steps")) .unwrap_or(false) { serde_json::from_value::<MultiStepMetaData>(metadata_value) } else { serde_json::from_value::<SingleTestMetaData>(metadata_value) .map(|s| s.into_multi()) } .with_context(|| format!("Failed to parse test spec: {}", metadata_string)) .unwrap() } fn test_context_from_metadata( metadata: &MultiStepMetaData, cwd: &PathRef, diagnostic_logger: Rc<RefCell<Vec<u8>>>, ) -> test_util::TestContext { let mut builder = TestContextBuilder::new(); builder = builder.logging_capture(diagnostic_logger); if metadata.temp_dir { builder = builder.use_temp_cwd(); } else { builder = builder.cwd(cwd.to_string_lossy()); } if metadata.canonicalized_temp_dir { // not actually deprecated, we just want to discourage its use #[allow( deprecated, reason = "not actually deprecated, just discouraging use" )] { builder = builder.use_canonicalized_temp_dir(); } } if metadata.symlinked_temp_dir { // not actually deprecated, we just want to discourage its use // because it's mostly used for testing purposes locally #[allow( deprecated, reason = "not actually deprecated, just discouraging use" )] { builder = builder.use_symlinked_temp_dir(); } if cfg!(not(debug_assertions)) { // panic to prevent using this on the CI as CI already uses // a symlinked temp directory for every test panic!("Cannot use symlinkedTempDir in release mode"); } } match &metadata.base { // todo(dsherret): add bases in the future as needed Some(base) => panic!("Unknown test base: {}", base), None => { // by default add all these builder = builder .add_jsr_env_vars() .add_npm_env_vars() .add_compile_env_vars(); } } let context = builder.build(); if metadata.temp_dir { // copy all the files in the cwd to a temp directory // excluding the metadata and assertion files let temp_dir = context.temp_dir().path(); let assertion_paths = resolve_test_and_assertion_files(cwd, metadata); cwd.copy_to_recursive_with_exclusions(temp_dir, &assertion_paths); } context } fn should_run(if_cond: Option<&str>) -> bool { if let Some(cond) = if_cond { match cond { "windows" => cfg!(windows), "unix" => cfg!(unix), "mac" => cfg!(target_os = "macos"), "linux" => cfg!(target_os = "linux"), "notCI" => std::env::var_os("CI").is_none(), "notMacIntel" => { cfg!(unix) && !(cfg!(target_os = "macos") && cfg!(target_arch = "x86_64")) } "notWindowsArm" => { !(cfg!(target_os = "windows") && cfg!(target_arch = "aarch64")) } "notWindowsArmOrMacIntel" => { !((cfg!(target_os = "windows") && cfg!(target_arch = "aarch64")) || (cfg!(target_os = "macos") && cfg!(target_arch = "x86_64"))) } "notSlowCiShard" => { let is_linux_x86_ci = *IS_CI && cfg!(target_os = "linux") && cfg!(target_arch = "x86_64"); !((cfg!(target_os = "windows") && cfg!(target_arch = "aarch64")) || (cfg!(target_os = "macos") && cfg!(target_arch = "x86_64")) || is_linux_x86_ci) } value => panic!("Unknown if condition: {}", value), } } else { true } } fn run_step( step: &StepMetaData, metadata: &MultiStepMetaData, cwd: &PathRef, context: &test_util::TestContext, ) { let substs = variant_substitutions(&step.variants, &metadata.variants); let command = if substs.is_empty() { let envs = metadata.envs.iter().chain(step.envs.iter()); context.new_command().envs(envs) } else { let mut envs = metadata .envs .iter() .chain(step.envs.iter()) .map(|(key, value)| (key.clone(), value.clone())) .collect::<HashMap<_, _>>(); substitute_variants_into_envs(&substs, &mut envs); context.new_command().envs(envs) }; let command = match &step.args { VecOrString::Vec(args) => { if substs.is_empty() { command.args_vec(args) } else { let mut args_replaced = args.clone(); for arg in args { for (from, to) in &substs { let arg_replaced = arg.replace(from, to); if arg_replaced.is_empty() && &arg_replaced != arg { continue; } args_replaced.push(arg_replaced); } } command.args_vec(args) } } VecOrString::String(text) => { if substs.is_empty() { command.args(text) } else { let text = apply_substs(text, &substs); command.args(text.as_ref()) } } }; let command = match step.cwd.as_ref().or(metadata.cwd.as_ref()) { Some(cwd) => command.current_dir(cwd), None => command, }; let command = match &step.command_name { Some(command_name) => { if substs.is_empty() { command.name(command_name) } else { let command_name = apply_substs(command_name, &substs); command.name(command_name.as_ref()) } } None => command, }; let command = match *NO_CAPTURE { // deprecated is only to prevent use, so this is fine here #[allow( deprecated, reason = "not actually deprecated, just discouraging use" )] true => command.show_output(), false => command, }; let command = match &step.input { Some(input) => { if input.ends_with(".in") { let test_input_path = cwd.join(input); command.stdin_text(std::fs::read_to_string(test_input_path).unwrap()) } else { command.stdin_text(input) } } None => command, }; let command = match metadata.timeout { Some(secs) => command.timeout(std::time::Duration::from_secs(secs)), None => command, }; let output = command.run(); let step_output = { if substs.is_empty() { step.output.clone() } else { let mut output = step.output.clone(); for (from, to) in substs { output = output.replace(&from, &to); } output } }; if step_output.ends_with(".out") { let test_output_path = cwd.join(&step_output); output.assert_matches_file(test_output_path); } else { assert!( step_output.len() <= 160, "The \"output\" property in your __test__.jsonc file is too long. Please extract this to an `.out` file to improve readability." ); output.assert_matches_text(&step_output); } output.assert_exit_code(step.exit_code); } fn resolve_test_and_assertion_files( dir: &PathRef, metadata: &MultiStepMetaData, ) -> HashSet<PathRef> { let mut result = HashSet::with_capacity(metadata.steps.len() + 1); result.insert(dir.join(MANIFEST_FILE_NAME)); result.extend(metadata.steps.iter().map(|step| dir.join(&step.output))); result } fn map_variants<'a, I>( test_data: &serde_json::Value, test_path: &Path, test_name: &str, variants: I, ) -> Result<CollectedCategoryOrTest<serde_json::Value>, CollectTestsError> where I: IntoIterator<Item = (&'a String, &'a serde_json::Value)>, { let mut children = Vec::with_capacity(2); for (variant_name, variant_data) in variants { let mut child_data = test_data.clone(); let child_obj = child_data .as_object_mut() .unwrap() .get_mut("variants") .unwrap() .as_object_mut() .unwrap(); child_obj.clear(); child_obj.insert(variant_name.clone(), variant_data.clone()); children.push(CollectedCategoryOrTest::Test(CollectedTest { name: format!("{}::{}", test_name, variant_name), path: test_path.to_path_buf(), line_and_column: None, data: child_data, })); } Ok(CollectedCategoryOrTest::Category(CollectedTestCategory { children, name: test_name.to_string(), path: test_path.to_path_buf(), })) } /// Maps a __test__.jsonc file to a category of tests if it contains a "test" object. fn map_test_within_file( test: CollectedTest, ) -> Result<CollectedCategoryOrTest<serde_json::Value>, CollectTestsError> { let test_path = PathRef::new(&test.path); let metadata_value = test_path.read_jsonc_value(); if metadata_value .as_object() .map(|o| o.contains_key("tests")) .unwrap_or(false) { let data: MultiTestMetaData = serde_json::from_value(metadata_value) .with_context(|| format!("Failed deserializing {}", test_path)) .map_err(CollectTestsError::Other)?; Ok(CollectedCategoryOrTest::Category(CollectedTestCategory { children: data.into_collected_tests(&test), name: test.name, path: test.path, })) } else if let Some(variants) = metadata_value .as_object() .and_then(|o| o.get("variants")) .and_then(|v| v.as_object()) && !variants.is_empty() { map_variants(&metadata_value, &test.path, &test.name, variants.iter()) } else { Ok(CollectedCategoryOrTest::Test(CollectedTest { name: test.name, path: test.path, line_and_column: None, data: metadata_value, })) } } // in the future we could consider using https://docs.rs/aho_corasick to do multiple replacements at once // in practice, though, i suspect the numbers here will be small enough that the naive approach is fast enough fn variant_substitutions( variants: &BTreeMap<String, JsonMap>, multi_step_variants: &BTreeMap<String, JsonMap>, ) -> Vec<(String, String)> { if variants.is_empty() && multi_step_variants.is_empty() { return Vec::new(); } let mut variant = variants.values().next().cloned().unwrap_or_default(); let multi_step_variant = multi_step_variants .values() .next() .cloned() .unwrap_or_default(); for (name, value) in multi_step_variant { if !variant.contains_key(&name) { variant.insert(name, value.clone()); } } let mut pairs = variant .into_iter() .filter_map(|(name, value)| { value .as_str() .map(|value| (format!("${{{}}}", name), value.to_string())) }) .collect::<Vec<_>>(); pairs.sort_by(|a, b| a.0.cmp(&b.0).reverse()); pairs } fn substitute_variants_into_envs( pairs: &Vec<(String, String)>, envs: &mut HashMap<String, String>, ) { let mut to_remove = Vec::new(); for (key, value) in pairs { for (k, v) in envs.iter_mut() { let replaced = v.replace(key.as_str(), value); if replaced.is_empty() && &replaced != v { to_remove.push(k.clone()); continue; } *v = replaced; } } for key in to_remove { envs.remove(&key); } } fn apply_substs<'a>( text: &'a str, substs: &'_ [(String, String)], ) -> Cow<'a, str> { if substs.is_empty() { Cow::Borrowed(text) } else { let mut text = Cow::Borrowed(text); for (from, to) in substs { text = text.replace(from, to).into(); } text } }