diff --git a/Cargo.lock b/Cargo.lock index caf08406..7fe57c85 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1017,6 +1017,7 @@ dependencies = [ "tracing", "tsify-next", "uuid", + "walkdir", "wasm-bindgen", ] diff --git a/cli-tests/src/upload.rs b/cli-tests/src/upload.rs index 13f7ec92..498f2f3e 100644 --- a/cli-tests/src/upload.rs +++ b/cli-tests/src/upload.rs @@ -21,7 +21,7 @@ use lazy_static::lazy_static; use predicates::prelude::*; use pretty_assertions::assert_eq; use prost::Message; -use tempfile::tempdir; +use tempfile::{tempdir, TempDir}; #[cfg(target_os = "macos")] use test_utils::inputs::unpack_archive_to_dir; use test_utils::{ @@ -39,9 +39,9 @@ use crate::utils::{ // NOTE: must be multi threaded to start a mock server #[tokio::test(flavor = "multi_thread")] async fn upload_bundle() { - let temp_dir = tempdir().unwrap(); + let temp_dir = TempDir::with_prefix("not-hidden").unwrap(); generate_mock_git_repo(&temp_dir); - generate_mock_valid_junit_xmls(&temp_dir); + generate_mock_valid_junit_xmls(temp_dir.path()); generate_mock_codeowners(&temp_dir); let state = MockServerBuilder::new().spawn_mock_server().await; @@ -1403,7 +1403,7 @@ enum CreateBundleResponse { } #[tokio::test(flavor = "multi_thread")] -async fn do_not_quarantines_tests_when_quarantine_disabled_set() { +async fn do_not_quarantine_tests_when_quarantine_disabled_set() { let temp_dir = tempdir().unwrap(); generate_mock_git_repo(&temp_dir); generate_mock_valid_junit_xmls(&temp_dir); @@ -1626,6 +1626,66 @@ async fn uses_passed_exit_code_if_unquarantined_tests_fail() { println!("{assert}"); } +#[tokio::test(flavor = "multi_thread")] +async fn uploaded_file_contains_updated_test_files() { + let temp_dir = TempDir::with_prefix("not_hidden").unwrap(); + generate_mock_git_repo(&temp_dir); + + let inner_dir = temp_dir.path().join("inner_dir"); + fs::create_dir(inner_dir).unwrap(); + + let test_location = temp_dir.path().join("inner_dir").join("test_file.ts"); + let mut test_file = fs::File::create(test_location).unwrap(); + write!(test_file, r#"it("does stuff", x)"#).unwrap(); + + let junit_location = temp_dir.path().join("junit_file.xml"); + let mut junit_file = fs::File::create(junit_location).unwrap(); + write!(junit_file, r#" + + + + + + + + "#).unwrap(); + + let state = MockServerBuilder::new().spawn_mock_server().await; + let assert = CommandBuilder::upload(temp_dir.path(), state.host.clone()) + .junit_paths("junit_file.xml") + .command() + .assert() + .success(); + + let requests = state.requests.lock().unwrap().clone(); + assert_eq!(requests.len(), 3); + + let file_upload = assert_matches!(requests.get(1).unwrap(), RequestPayload::S3Upload(d) => d); + let file = fs::File::open(file_upload.join("meta.json")).unwrap(); + let reader = BufReader::new(file); + let bundle_meta: BundleMeta = serde_json::from_reader(reader).unwrap(); + let internal_bundled_file = bundle_meta.internal_bundled_file.as_ref().unwrap(); + let bin = fs::read(file_upload.join(&internal_bundled_file.path)).unwrap(); + let report = proto::test_context::test_run::TestResult::decode(&*bin).unwrap(); + + assert_eq!(report.test_case_runs.len(), 1); + let test_case_run = &report.test_case_runs.first().unwrap(); + assert_eq!(test_case_run.classname, "test_file.ts"); + let expected_file = temp_dir + .path() + .canonicalize() + .unwrap() + .join("inner_dir") + .join("test_file.ts") + .to_str() + .unwrap() + .to_string(); + assert_eq!(test_case_run.file, String::from("test_file.ts")); + assert_eq!(test_case_run.detected_file, expected_file); + + println!("{assert}"); +} + #[tokio::test(flavor = "multi_thread")] async fn does_not_print_exit_code_with_validation_reports_none() { let temp_dir = tempdir().unwrap(); diff --git a/cli-tests/src/utils.rs b/cli-tests/src/utils.rs index 3335b62b..5fdcccc3 100644 --- a/cli-tests/src/utils.rs +++ b/cli-tests/src/utils.rs @@ -41,16 +41,18 @@ fn generate_mock_valid_junit_mocker() -> JunitMock { JunitMock::new(junit_mock::Options::default()) } -pub fn generate_mock_valid_junit_xmls>(directory: T) -> Vec { +pub fn generate_mock_valid_junit_xmls + Clone>(directory: T) -> Vec { let mut jm = generate_mock_valid_junit_mocker(); - let reports = jm.generate_reports(); + let tmp_dir = Some(directory.clone()); + let reports = jm.generate_reports(&tmp_dir); jm.write_reports_to_file(directory.as_ref(), reports) .unwrap() } -pub fn generate_mock_bazel_bep>(directory: T) -> PathBuf { +pub fn generate_mock_bazel_bep + Clone>(directory: T) -> PathBuf { let mut jm = generate_mock_valid_junit_mocker(); - let reports = jm.generate_reports(); + let tmp_dir = Some(directory.clone()); + let reports = jm.generate_reports(&tmp_dir); let mock_junits = jm .write_reports_to_file(directory.as_ref(), &reports) .unwrap(); @@ -128,33 +130,36 @@ pub fn generate_mock_bazel_bep>(directory: T) -> PathBuf { file_path } -pub fn generate_mock_invalid_junit_xmls>(directory: T) { +pub fn generate_mock_invalid_junit_xmls + Clone>(directory: T) { let mut jm_options = junit_mock::Options::default(); jm_options.test_suite.test_suite_names = Some(vec!["".to_string()]); jm_options.global.timestamp = Utc::now() .fixed_offset() .checked_sub_signed(TimeDelta::minutes(1)); let mut jm = JunitMock::new(jm_options); - let reports = jm.generate_reports(); + let tmp_dir = Some(directory.clone()); + let reports = jm.generate_reports(&tmp_dir); jm.write_reports_to_file(directory.as_ref(), reports) .unwrap(); } -pub fn generate_mock_suboptimal_junit_xmls>(directory: T) { +pub fn generate_mock_suboptimal_junit_xmls + Clone>(directory: T) { let mut jm_options = junit_mock::Options::default(); jm_options.global.timestamp = Utc::now() .fixed_offset() .checked_sub_signed(TimeDelta::hours(24)); let mut jm = JunitMock::new(jm_options); - let reports = jm.generate_reports(); + let tmp_dir = Some(directory.clone()); + let reports = jm.generate_reports(&tmp_dir); jm.write_reports_to_file(directory.as_ref(), reports) .unwrap(); } -pub fn generate_mock_missing_filepath_suboptimal_junit_xmls>(directory: T) { +pub fn generate_mock_missing_filepath_suboptimal_junit_xmls + Clone>(directory: T) { let jm_options = junit_mock::Options::default(); let mut jm = JunitMock::new(jm_options); - let mut reports = jm.generate_reports(); + let tmp_dir = Some(directory.clone()); + let mut reports = jm.generate_reports(&tmp_dir); for report in reports.iter_mut() { for testsuite in report.test_suites.iter_mut() { for test_case in testsuite.test_cases.iter_mut() { diff --git a/cli-tests/src/validate.rs b/cli-tests/src/validate.rs index 071661f0..501787df 100644 --- a/cli-tests/src/validate.rs +++ b/cli-tests/src/validate.rs @@ -3,12 +3,12 @@ use superconsole::{ style::{style, Color, Stylize}, Line, Span, }; -use tempfile::tempdir; +use tempfile::{tempdir, TempDir}; use crate::{ command_builder::CommandBuilder, utils::{ - generate_mock_codeowners, generate_mock_invalid_junit_xmls, + generate_mock_codeowners, generate_mock_git_repo, generate_mock_invalid_junit_xmls, generate_mock_missing_filepath_suboptimal_junit_xmls, generate_mock_suboptimal_junit_xmls, generate_mock_valid_junit_xmls, write_junit_xml_to_dir, }, @@ -170,7 +170,7 @@ fn validate_invalid_xml() { #[test] fn validate_suboptimal_junits() { - let temp_dir = tempdir().unwrap(); + let temp_dir = TempDir::with_prefix("not-hidden").unwrap(); generate_mock_suboptimal_junit_xmls(&temp_dir); let assert = CommandBuilder::validate(temp_dir.path()) @@ -196,6 +196,7 @@ fn validate_suboptimal_junits() { #[test] fn validate_missing_filepath_suboptimal_junits() { let temp_dir = tempdir().unwrap(); + generate_mock_git_repo(&temp_dir); generate_mock_missing_filepath_suboptimal_junit_xmls(&temp_dir); generate_mock_codeowners(&temp_dir); diff --git a/cli/src/context.rs b/cli/src/context.rs index c47acb5b..0a4499e2 100644 --- a/cli/src/context.rs +++ b/cli/src/context.rs @@ -215,6 +215,7 @@ pub fn generate_internal_file( file_sets: &[FileSet], temp_dir: &TempDir, codeowners: Option<&CodeOwners>, + repo: &BundleRepo, show_warnings: bool, variant: Option, ) -> anyhow::Result<( @@ -254,10 +255,11 @@ pub fn generate_internal_file( Ok(validate( &reports[0], file_set.test_runner_report.map(|t| t.into()), + repo, )), ); } - test_case_runs.extend(junit_parser.into_test_case_runs(codeowners)); + test_case_runs.extend(junit_parser.into_test_case_runs(codeowners, repo)); } } } diff --git a/cli/src/upload_command.rs b/cli/src/upload_command.rs index 1c00cb8d..b9fa9c64 100644 --- a/cli/src/upload_command.rs +++ b/cli/src/upload_command.rs @@ -317,6 +317,7 @@ pub async fn run_upload( &meta.base_props.file_sets, &temp_dir, meta.base_props.codeowners.as_ref(), + &meta.base_props.repo, // hide warnings on parsed xcresult output #[cfg(target_os = "macos")] upload_args.xcresult_path.is_none(), diff --git a/cli/src/validate_command.rs b/cli/src/validate_command.rs index 67c6c733..7716f6c1 100644 --- a/cli/src/validate_command.rs +++ b/cli/src/validate_command.rs @@ -19,6 +19,7 @@ use context::{ JunitValidationLevel, }, }, + repo::BundleRepo, }; use display::end_output::EndOutput; use pluralizer::pluralize; @@ -537,6 +538,7 @@ async fn validate( (parsed_reports, parse_issues) }, ); + let repo = BundleRepo::new(None, None, None, None, None, None, false).unwrap_or_default(); let file_parse_issues = gen_parse_issues(parse_issues); let report_validations: JunitFileToValidation = parsed_reports @@ -544,7 +546,11 @@ async fn validate( .map(|(file, (report, test_runner_report))| { ( file, - validate_report(&report, test_runner_report.map(TestRunnerReport::from)), + validate_report( + &report, + test_runner_report.map(TestRunnerReport::from), + &repo, + ), ) }) .collect(); diff --git a/context-js/src/lib.rs b/context-js/src/lib.rs index 72c045ac..634d482c 100644 --- a/context-js/src/lib.rs +++ b/context-js/src/lib.rs @@ -107,6 +107,7 @@ pub fn junit_validate( junit::bindings::BindingsJunitReportValidation::from(junit::validator::validate( &report.clone().into(), test_runner_report.map(junit::junit_path::TestRunnerReport::from), + &repo::BundleRepo::default(), )) } diff --git a/context-py/src/lib.rs b/context-py/src/lib.rs index e1727a5b..e38dda6f 100644 --- a/context-py/src/lib.rs +++ b/context-py/src/lib.rs @@ -125,6 +125,7 @@ fn junit_validate( junit::bindings::BindingsJunitReportValidation::from(junit::validator::validate( &report.into(), test_runner_report.map(TestRunnerReport::from), + &repo::BundleRepo::default(), )) } diff --git a/context/Cargo.toml b/context/Cargo.toml index d5f5f60c..9645d526 100644 --- a/context/Cargo.toml +++ b/context/Cargo.toml @@ -38,6 +38,7 @@ prost-wkt-types = { version = "0.5.1", features = ["vendored-protox"] } tracing = "0.1.41" prost = "0.12.6" codeowners = { version = "0.1.3", path = "../codeowners" } +walkdir = "2.5.0" [target.'cfg(target_os = "linux")'.dependencies] pyo3 = { version = "0.22.5", optional = true, features = [ diff --git a/context/src/junit/bindings.rs b/context/src/junit/bindings.rs index ae13da87..f78b1bc8 100644 --- a/context/src/junit/bindings.rs +++ b/context/src/junit/bindings.rs @@ -166,6 +166,7 @@ impl From for BindingsTestCase { attempt_number, is_quarantined, codeowners, + detected_file: _detected_file, }: TestCaseRun, ) -> Self { let started_at = started_at.unwrap_or_default(); @@ -1067,8 +1068,11 @@ mod tests { #[test] fn parse_test_report_to_bindings() { use prost_wkt_types::Timestamp; + use tempfile::TempDir; - use crate::junit::validator::validate; + use crate::{junit::validator::validate, repo::BundleRepo}; + + let temp_dir = TempDir::with_prefix("not-hidden").unwrap(); let test_started_at = Timestamp { seconds: 1000, nanos: 0, @@ -1080,11 +1084,13 @@ mod tests { let codeowner1 = CodeOwner { name: "@user".into(), }; + let test_file = temp_dir.path().join("test_file"); + let file_str = String::from(test_file.as_os_str().to_str().unwrap()); let test1 = TestCaseRun { id: "test_id1".into(), name: "test_name".into(), classname: "test_classname".into(), - file: "test_file".into(), + file: file_str.clone(), parent_name: "test_parent_name1".into(), line: 1, status: TestCaseRunStatus::Success.into(), @@ -1100,7 +1106,7 @@ mod tests { id: "test_id2".into(), name: "test_name".into(), classname: "test_classname".into(), - file: "test_file".into(), + file: file_str, parent_name: "test_parent_name2".into(), line: 1, status: TestCaseRunStatus::Failure.into(), @@ -1188,7 +1194,11 @@ mod tests { assert_eq!(test_case2.codeowners.clone().unwrap().len(), 0); // verify that the test report is valid - let results = validate(&converted_bindings.clone().into(), None); + let results = validate( + &converted_bindings.clone().into(), + None, + &BundleRepo::default(), + ); assert_eq!(results.all_issues_flat().len(), 1); results .all_issues_flat() @@ -1224,6 +1234,8 @@ mod tests { #[cfg(feature = "bindings")] #[test] fn test_junit_conversion_paths() { + use crate::repo::BundleRepo; + let mut junit_parser = JunitParser::new(); let file_contents = r#" @@ -1242,7 +1254,7 @@ mod tests { assert!(parsed_results.is_ok()); // Get test case runs from parser - let test_case_runs = junit_parser.into_test_case_runs(None); + let test_case_runs = junit_parser.into_test_case_runs(None, &BundleRepo::default()); assert_eq!(test_case_runs.len(), 2); // Convert test case runs to bindings diff --git a/context/src/junit/file_extractor.rs b/context/src/junit/file_extractor.rs new file mode 100644 index 00000000..5e080b9e --- /dev/null +++ b/context/src/junit/file_extractor.rs @@ -0,0 +1,419 @@ +use std::fs; +use std::path::Path; + +use quick_junit::{TestCase, XmlString}; +use walkdir::{DirEntry, WalkDir}; + +use super::parser::extra_attrs; +use crate::repo::BundleRepo; + +fn not_hidden(entry: &DirEntry) -> bool { + entry + .file_name() + .to_str() + .map(|file_string| !file_string.starts_with('.')) + .unwrap_or(true) +} + +fn contains_test(path: &String, test_xml_name: &XmlString) -> bool { + let test_name = test_xml_name.as_str(); + // Frameworks like vitest handle tests with an it() call inside a describe() call by + // using "{describe_name} > {it_name}" as the test name, so we split on that in order + // to get substrings that we can search for. + let mut test_parts = test_name.split(" > "); + fs::read_to_string(Path::new(path)) + .ok() + .map(|text| { + let has_full_name = text.contains(test_name); + let has_name_splits = test_parts.all(|p| text.contains(p)); + has_full_name || has_name_splits + }) + .unwrap_or(false) +} + +fn file_containing_tests(file_paths: Vec, test_name: &XmlString) -> Option { + let mut matching_paths = file_paths + .iter() + .filter(|path| contains_test(path, test_name)); + let first_match = matching_paths.next(); + let another_match = matching_paths.next(); + match (first_match, another_match) { + (None, _) => None, + (Some(only_match), None) => Some((*only_match).clone()), + (_, _) => None, + } +} + +// None if is not a file or file does not exist, Some(absolute path) if it does exist in the root +fn convert_to_absolute( + initial: &XmlString, + repo: &BundleRepo, + test_name: &XmlString, +) -> Option { + let initial_str = String::from(initial.as_str()); + let path = Path::new(&initial_str); + let repo_root_path = Path::new(&repo.repo_root); + if path.is_absolute() { + path.to_str().map(String::from) + } else if repo_root_path.is_absolute() && repo_root_path.exists() { + let mut walk = WalkDir::new(repo.repo_root.clone()) + .into_iter() + .filter_entry(not_hidden) + .filter_map(|result| { + if let Ok(entry) = result { + if entry.path().ends_with(path) { + entry.path().as_os_str().to_str().map(String::from).clone() + } else { + None + } + } else { + None + } + }); + let first_match = walk.next(); + let another_match = walk.next(); + match (first_match, another_match) { + (None, _) => None, + (Some(only_match), None) => Some(only_match), + (Some(first_match), Some(second_match)) => file_containing_tests( + [vec![first_match, second_match], walk.collect()].concat(), + test_name, + ), + } + } else if path + .file_name() + .iter() + .flat_map(|os| os.to_str()) + .all(|name| name.contains('.')) + { + Some(initial_str) + } else { + None + } +} + +fn validate_as_filename(initial: &XmlString) -> Option { + let initial_str = String::from(initial.as_str()); + let path = Path::new(&initial_str); + if path.extension().is_some() { + Some(initial_str) + } else { + None + } +} + +pub fn filename_for_test_case(test_case: &TestCase) -> String { + test_case + .extra + .get(extra_attrs::FILE) + .or(test_case.extra.get(extra_attrs::FILEPATH)) + .or(test_case.classname.as_ref()) + .iter() + .flat_map(|s| validate_as_filename(s)) + .next() + .unwrap_or_default() +} + +pub fn detected_file_for_test_case(test_case: &TestCase, repo: &BundleRepo) -> String { + test_case + .extra + .get(extra_attrs::FILE) + .or(test_case.extra.get(extra_attrs::FILEPATH)) + .or(test_case.classname.as_ref()) + .iter() + .flat_map(|s| convert_to_absolute(s, repo, &test_case.name)) + .next() + .unwrap_or_default() +} + +#[cfg(test)] +mod tests { + use std::fs; + use std::path::PathBuf; + + use quick_junit::XmlString; + use tempfile::{tempdir, TempDir}; + + use super::*; + use crate::repo::RepoUrlParts; + + fn stringify(fp: PathBuf) -> String { + String::from(fp.as_os_str().to_str().unwrap()) + } + + fn bundle_repo(dir: &Path) -> BundleRepo { + BundleRepo { + repo: RepoUrlParts::default(), + repo_root: String::from(dir.as_os_str().to_str().unwrap()), + repo_url: String::from(""), + repo_head_sha: String::from(""), + repo_head_sha_short: None, + repo_head_branch: String::from(""), + repo_head_commit_epoch: 0, + repo_head_commit_message: String::from(""), + repo_head_author_name: String::from(""), + repo_head_author_email: String::from(""), + } + } + + #[test] + fn test_contains_test_if_unsplit_test_name_present() { + let temp_dir = tempdir().unwrap(); + let file_path = temp_dir.as_ref().join("match.txt"); + let text = r#" + describe("description", { + it("my_super_good_test", { + }) + }) + "#; + fs::write(file_path.clone(), text).unwrap(); + let actual = contains_test(&stringify(file_path), &XmlString::new("my_super_good_test")); + assert!(actual); + } + + #[test] + fn test_contains_test_if_split_test_name_present() { + let temp_dir = tempdir().unwrap(); + let file_path = temp_dir.as_ref().join("match.txt"); + let text = r#" + describe("description", { + it("my_super_good_test", { + }) + }) + "#; + fs::write(file_path.clone(), text).unwrap(); + let actual = contains_test( + &stringify(file_path), + &XmlString::new("description > my_super_good_test"), + ); + assert!(actual); + } + + #[test] + fn test_contains_test_if_part_of_split_test_name_present() { + let temp_dir = tempdir().unwrap(); + let file_path = temp_dir.as_ref().join("match.txt"); + let text = r#" + it("my_super_good_test", { + }) + "#; + fs::write(file_path.clone(), text).unwrap(); + let actual = contains_test( + &stringify(file_path), + &XmlString::new("description > my_super_good_test"), + ); + assert!(!actual); + } + + #[test] + fn test_contains_test_if_test_name_not_present() { + let temp_dir = tempdir().unwrap(); + let file_path = temp_dir.as_ref().join("match.txt"); + let text = r#" + describe("description", { + it("my_super_good_test", { + }) + }) + "#; + fs::write(file_path.clone(), text).unwrap(); + let actual = contains_test(&stringify(file_path), &XmlString::new("totally_different")); + assert!(!actual); + } + + #[test] + fn test_file_containing_test_if_none_contains_test() { + let temp_dir = tempdir().unwrap(); + let file_path_1 = temp_dir.as_ref().join("file_1.txt"); + let text_1 = r#"it("test_1")"#; + fs::write(file_path_1.clone(), text_1).unwrap(); + let file_path_2 = temp_dir.as_ref().join("file_2.txt"); + let text_2 = r#"it("test_2")"#; + fs::write(file_path_2.clone(), text_2).unwrap(); + let file_path_3 = temp_dir.as_ref().join("file_3.txt"); + let text_3 = r#"it("test_3")"#; + fs::write(file_path_3.clone(), text_3).unwrap(); + let actual = file_containing_tests( + vec![ + stringify(file_path_1), + stringify(file_path_2), + stringify(file_path_3), + ], + &XmlString::new("totally_different"), + ); + assert_eq!(actual, None); + } + + #[test] + fn test_file_containing_test_if_one_contains_test() { + let temp_dir = tempdir().unwrap(); + let file_path_1 = temp_dir.as_ref().join("file_1.txt"); + let text_1 = r#"it("test_1")"#; + fs::write(file_path_1.clone(), text_1).unwrap(); + let file_path_2 = temp_dir.as_ref().join("file_2.txt"); + let text_2 = r#"it("test_2")"#; + fs::write(file_path_2.clone(), text_2).unwrap(); + let file_path_3 = temp_dir.as_ref().join("file_3.txt"); + let text_3 = r#"it("test_3")"#; + fs::write(file_path_3.clone(), text_3).unwrap(); + let actual = file_containing_tests( + vec![ + stringify(file_path_1), + stringify(file_path_2.clone()), + stringify(file_path_3), + ], + &XmlString::new("test_2"), + ); + assert_eq!(actual, Some(stringify(file_path_2))); + } + + #[test] + fn test_file_containing_test_if_multiple_contain_test() { + let temp_dir = tempdir().unwrap(); + let file_path_1 = temp_dir.as_ref().join("file_1.txt"); + let text_1 = r#"it("common_test")"#; + fs::write(file_path_1.clone(), text_1).unwrap(); + let file_path_2 = temp_dir.as_ref().join("file_2.txt"); + let text_2 = r#"it("common_test")"#; + fs::write(file_path_2.clone(), text_2).unwrap(); + let file_path_3 = temp_dir.as_ref().join("file_3.txt"); + let text_3 = r#"it("test_3")"#; + fs::write(file_path_3.clone(), text_3).unwrap(); + let actual = file_containing_tests( + vec![ + stringify(file_path_1), + stringify(file_path_2), + stringify(file_path_3), + ], + &XmlString::new("common_test"), + ); + assert_eq!(actual, None); + } + + #[test] + fn test_convert_to_absolute_when_no_repo_root() { + let temp_dir = TempDir::with_prefix("not-hidden").unwrap(); + let file_path = temp_dir.as_ref().join("test.txt"); + let text = r#"it("test")"#; + fs::write(file_path.clone(), text).unwrap(); + let actual = convert_to_absolute( + &XmlString::new("test.txt"), + &BundleRepo::default(), + &XmlString::new("test"), + ); + assert_eq!(actual, Some(String::from("test.txt"))); + } + + #[test] + fn test_convert_to_absolute_when_already_absolute() { + let temp_dir = TempDir::with_prefix("not-hidden").unwrap(); + let file_path = temp_dir.as_ref().join("test.txt"); + let text = r#"it("test")"#; + fs::write(file_path.clone(), text).unwrap(); + let actual = convert_to_absolute( + &XmlString::new(stringify(file_path.clone())), + &bundle_repo(temp_dir.as_ref()), + &XmlString::new("test"), + ); + assert_eq!(actual, Some(stringify(file_path))); + } + + #[test] + fn test_convert_to_absolute_when_no_file_matches() { + let temp_dir = TempDir::with_prefix("not-hidden").unwrap(); + let file_path_1 = temp_dir.as_ref().join("file_1.txt"); + let text_1 = r#"it("test_1")"#; + fs::write(file_path_1.clone(), text_1).unwrap(); + let file_path_2 = temp_dir.as_ref().join("file_2.txt"); + let text_2 = r#"it("test_2")"#; + fs::write(file_path_2.clone(), text_2).unwrap(); + let file_path_3 = temp_dir.as_ref().join("file_3.txt"); + let text_3 = r#"it("test_3")"#; + fs::write(file_path_3.clone(), text_3).unwrap(); + let actual = convert_to_absolute( + &XmlString::new("not_a_test.txt"), + &bundle_repo(temp_dir.as_ref()), + &XmlString::new("test"), + ); + assert_eq!(actual, None); + } + + #[test] + fn test_convert_to_absolute_when_one_file_matches() { + let temp_dir = TempDir::with_prefix("not-hidden").unwrap(); + let inner_dir = "inner_dir"; + fs::create_dir(temp_dir.as_ref().join(inner_dir)).unwrap(); + let file_path_1 = temp_dir.as_ref().join(inner_dir).join("file_1.txt"); + let text_1 = r#"it("test_1")"#; + fs::write(file_path_1.clone(), text_1).unwrap(); + let file_path_2 = temp_dir.as_ref().join(inner_dir).join("file_2.txt"); + let text_2 = r#"it("test_2")"#; + fs::write(file_path_2.clone(), text_2).unwrap(); + let file_path_3 = temp_dir.as_ref().join(inner_dir).join("file_3.txt"); + let text_3 = r#"it("test_3")"#; + fs::write(file_path_3.clone(), text_3).unwrap(); + let actual = convert_to_absolute( + &XmlString::new("file_1.txt"), + &bundle_repo(temp_dir.as_ref()), + &XmlString::new("test"), + ); + assert_eq!(actual, Some(stringify(file_path_1))); + } + + #[test] + fn test_convert_to_absolute_when_many_files_match_and_none_contain() { + let temp_dir = TempDir::with_prefix("not-hidden").unwrap(); + let inner_dir = "inner_dir"; + let other_dir = "other_dir"; + fs::create_dir(temp_dir.as_ref().join(inner_dir)).unwrap(); + fs::create_dir(temp_dir.as_ref().join(other_dir)).unwrap(); + let file_path_1 = temp_dir.as_ref().join(inner_dir).join("file.txt"); + let text_1 = r#"it("test_1")"#; + fs::write(file_path_1.clone(), text_1).unwrap(); + let file_path_2 = temp_dir.as_ref().join(other_dir).join("file.txt"); + let text_2 = r#"it("test_2")"#; + fs::write(file_path_2.clone(), text_2).unwrap(); + let actual = convert_to_absolute( + &XmlString::new("file.txt"), + &bundle_repo(temp_dir.as_ref()), + &XmlString::new("totally_different"), + ); + assert_eq!(actual, None); + } + + #[test] + fn test_convert_to_absolute_when_many_files_match_and_one_contains() { + let temp_dir = TempDir::with_prefix("not-hidden").unwrap(); + let inner_dir = "inner_dir"; + let other_dir = "other_dir"; + fs::create_dir(temp_dir.as_ref().join(inner_dir)).unwrap(); + fs::create_dir(temp_dir.as_ref().join(other_dir)).unwrap(); + let file_path_1 = temp_dir.as_ref().join(inner_dir).join("file.txt"); + let text_1 = r#"it("test_1")"#; + fs::write(file_path_1.clone(), text_1).unwrap(); + let file_path_2 = temp_dir.as_ref().join(other_dir).join("file.txt"); + let text_2 = r#"it("test_2")"#; + fs::write(file_path_2.clone(), text_2).unwrap(); + let actual = convert_to_absolute( + &XmlString::new("file.txt"), + &bundle_repo(temp_dir.as_ref()), + &XmlString::new("test_1"), + ); + assert_eq!(actual, Some(stringify(file_path_1))); + } + + #[test] + fn test_convert_to_absolute_when_only_match_is_in_hidden_directory() { + let temp_dir = TempDir::with_prefix("not-hidden").unwrap(); + let hidden_dir = ".hidden"; + fs::create_dir(temp_dir.as_ref().join(hidden_dir)).unwrap(); + let file_path = temp_dir.as_ref().join(hidden_dir).join("test.txt"); + let text = r#"it("test")"#; + fs::write(file_path.clone(), text).unwrap(); + let actual = convert_to_absolute( + &XmlString::new("test.txt"), + &bundle_repo(temp_dir.as_ref()), + &XmlString::new("test"), + ); + assert_eq!(actual, None); + } +} diff --git a/context/src/junit/mod.rs b/context/src/junit/mod.rs index 23716c81..70457f9a 100644 --- a/context/src/junit/mod.rs +++ b/context/src/junit/mod.rs @@ -1,6 +1,7 @@ #[cfg(feature = "bindings")] pub mod bindings; mod date_parser; +mod file_extractor; pub mod junit_path; pub mod parser; pub mod validator; diff --git a/context/src/junit/parser.rs b/context/src/junit/parser.rs index 27e586c0..b4da66f6 100644 --- a/context/src/junit/parser.rs +++ b/context/src/junit/parser.rs @@ -21,6 +21,10 @@ use thiserror::Error; use wasm_bindgen::prelude::*; use super::date_parser::JunitDateParser; +use crate::{ + junit::file_extractor::{detected_file_for_test_case, filename_for_test_case}, + repo::BundleRepo, +}; const TAG_REPORT: &[u8] = b"testsuites"; const TAG_TEST_SUITE: &[u8] = b"testsuite"; @@ -192,11 +196,17 @@ impl JunitParser { self.reports } - pub fn into_test_case_runs(self, codeowners: Option<&CodeOwners>) -> Vec { + pub fn into_test_case_runs( + self, + codeowners: Option<&CodeOwners>, + repo: &BundleRepo, + ) -> Vec { let mut test_case_runs = Vec::new(); for report in self.reports { for test_suite in report.test_suites { for test_case in test_suite.test_cases { + let file = filename_for_test_case(&test_case); + let detected_file = detected_file_for_test_case(&test_case, repo); let mut test_case_run = TestCaseRun { name: test_case.name.into(), parent_name: test_suite.name.to_string(), @@ -259,12 +269,6 @@ impl JunitParser { TestCaseRunStatus::Failure.into() } }; - let file = test_case - .extra - .get(extra_attrs::FILE) - .or_else(|| test_case.extra.get(extra_attrs::FILEPATH)) - .map(|v| v.to_string()) - .unwrap_or_default(); if !file.is_empty() && codeowners.is_some() { let codeowners: Option> = codeowners .as_ref() @@ -277,6 +281,7 @@ impl JunitParser { } } test_case_run.file = file; + test_case_run.detected_file = detected_file; test_case_run.line = test_case .extra .get(extra_attrs::LINE) @@ -854,7 +859,7 @@ mod tests { use prost_wkt_types::Timestamp; use proto::test_context::test_run::TestCaseRunStatus; - use crate::junit::parser::JunitParser; + use crate::{junit::parser::JunitParser, repo::BundleRepo}; #[test] fn test_into_test_case_runs() { let mut junit_parser = JunitParser::new(); @@ -875,7 +880,7 @@ mod tests { "#; let parsed_results = junit_parser.parse(BufReader::new(file_contents.as_bytes())); assert!(parsed_results.is_ok()); - let test_case_runs = junit_parser.into_test_case_runs(None); + let test_case_runs = junit_parser.into_test_case_runs(None, &BundleRepo::default()); assert_eq!(test_case_runs.len(), 2); let test_case_run1 = &test_case_runs[0]; assert_eq!(test_case_run1.name, "test_variant_truncation1"); diff --git a/context/src/junit/validator.rs b/context/src/junit/validator.rs index 9f04adbc..b5e405c6 100644 --- a/context/src/junit/validator.rs +++ b/context/src/junit/validator.rs @@ -10,7 +10,8 @@ use thiserror::Error; #[cfg(feature = "wasm")] use wasm_bindgen::prelude::*; -use super::parser::extra_attrs; +use crate::junit::file_extractor::detected_file_for_test_case; +use crate::repo::BundleRepo; use crate::{ junit::junit_path::TestRunnerReport, string_safety::{validate_field_len, FieldLen}, @@ -79,6 +80,7 @@ impl Default for JunitValidationType { pub fn validate( report: &Report, test_runner_report: Option, + repo: &BundleRepo, ) -> JunitReportValidation { let mut report_validation = JunitReportValidation::default(); @@ -126,15 +128,9 @@ pub fn validate( } }; - match validate_field_len::( - test_case - .extra - .get(extra_attrs::FILE) - .or(test_case.extra.get(extra_attrs::FILEPATH)) - .as_ref() - .map(|s| s.as_str()) - .unwrap_or_default(), - ) { + match validate_field_len::(detected_file_for_test_case( + test_case, repo, + )) { FieldLen::Valid => (), FieldLen::TooShort(s) => { test_case_validation.add_issue(JunitValidationIssue::SubOptimal( diff --git a/context/tests/junit.rs b/context/tests/junit.rs index 0e2192ec..0a9fab60 100644 --- a/context/tests/junit.rs +++ b/context/tests/junit.rs @@ -1,18 +1,22 @@ use std::{fs, io::BufReader, time::Duration}; use chrono::{Days, NaiveTime, TimeDelta, Utc}; -use context::junit::{ +use context::{ self, - junit_path::{TestRunnerReport, TestRunnerReportStatus}, - parser::JunitParser, - validator::{ - JunitReportValidationIssue, JunitReportValidationIssueSubOptimal, - JunitTestCaseValidationIssue, JunitTestCaseValidationIssueInvalid, - JunitTestCaseValidationIssueSubOptimal, JunitTestSuiteValidationIssue, - JunitTestSuiteValidationIssueInvalid, JunitTestSuiteValidationIssueSubOptimal, - JunitValidationIssue, JunitValidationIssueType, JunitValidationLevel, - TestRunnerReportValidationIssue, TestRunnerReportValidationIssueSubOptimal, + junit::{ + self, + junit_path::{TestRunnerReport, TestRunnerReportStatus}, + parser::JunitParser, + validator::{ + JunitReportValidationIssue, JunitReportValidationIssueSubOptimal, + JunitTestCaseValidationIssue, JunitTestCaseValidationIssueInvalid, + JunitTestCaseValidationIssueSubOptimal, JunitTestSuiteValidationIssue, + JunitTestSuiteValidationIssueInvalid, JunitTestSuiteValidationIssueSubOptimal, + JunitValidationIssue, JunitValidationIssueType, JunitValidationLevel, + TestRunnerReportValidationIssue, TestRunnerReportValidationIssueSubOptimal, + }, }, + repo::BundleRepo, }; use junit_mock::JunitMock; use quick_junit::Report; @@ -57,7 +61,8 @@ fn generate_mock_junit_reports( let mut jm = JunitMock::new(options); let seed = jm.get_seed(); - let reports = jm.generate_reports(); + let tmp_dir: Option = None; + let reports = jm.generate_reports(&tmp_dir); (seed, reports) } @@ -90,7 +95,8 @@ fn validate_test_suite_name_too_short() { test_suite.name = String::new().into(); } - let report_validation = junit::validator::validate(&generated_report, None); + let report_validation = + junit::validator::validate(&generated_report, None, &BundleRepo::default()); assert_eq!( report_validation.max_level(), @@ -126,7 +132,8 @@ fn validate_test_case_name_too_short() { } } - let report_validation = junit::validator::validate(&generated_report, None); + let report_validation = + junit::validator::validate(&generated_report, None, &BundleRepo::default()); assert_eq!( report_validation.max_level(), @@ -159,7 +166,8 @@ fn validate_test_suite_name_too_long() { test_suite.name = "a".repeat(junit::validator::MAX_FIELD_LEN + 1).into(); } - let report_validation = junit::validator::validate(&generated_report, None); + let report_validation = + junit::validator::validate(&generated_report, None, &BundleRepo::default()); assert_eq!( report_validation.max_level(), @@ -195,7 +203,8 @@ fn validate_test_case_name_too_long() { } } - let report_validation = junit::validator::validate(&generated_report, None); + let report_validation = + junit::validator::validate(&generated_report, None, &BundleRepo::default()); assert_eq!( report_validation.max_level(), @@ -233,7 +242,8 @@ fn validate_max_level() { } } - let report_validation = junit::validator::validate(&generated_report, None); + let report_validation = + junit::validator::validate(&generated_report, None, &BundleRepo::default()); assert_eq!( report_validation.max_level(), @@ -304,7 +314,8 @@ fn validate_timestamps() { } } - let report_validation = junit::validator::validate(&generated_report, None); + let report_validation = + junit::validator::validate(&generated_report, None, &BundleRepo::default()); assert_eq!( report_validation.max_level(), @@ -338,7 +349,8 @@ fn validate_test_runner_report_overrides_timestamp() { options.global.timestamp = Some(old_timestamp.fixed_offset()); let mut jm = JunitMock::new(options); let seed = jm.get_seed(); - let mut generated_reports = jm.generate_reports(); + let tmp_dir: Option = None; + let mut generated_reports = jm.generate_reports(&tmp_dir); let generated_report = generated_reports.pop().unwrap(); @@ -351,8 +363,11 @@ fn validate_test_runner_report_overrides_timestamp() { .checked_add_signed(TimeDelta::minutes(1)) .unwrap(), }; - let override_report_validation = - junit::validator::validate(&generated_report, Some(override_report)); + let override_report_validation = junit::validator::validate( + &generated_report, + Some(override_report), + &BundleRepo::default(), + ); pretty_assertions::assert_eq!( override_report_validation.all_issues(), &[ @@ -388,8 +403,11 @@ fn validate_test_runner_report_overrides_timestamp() { .checked_add_signed(TimeDelta::minutes(1)) .unwrap(), }; - let override_report_validation = - junit::validator::validate(&generated_report, Some(override_report)); + let override_report_validation = junit::validator::validate( + &generated_report, + Some(override_report), + &BundleRepo::default(), + ); pretty_assertions::assert_eq!( override_report_validation.all_issues(), &[ @@ -425,8 +443,11 @@ fn validate_test_runner_report_overrides_timestamp() { .checked_add_signed(TimeDelta::minutes(1)) .unwrap(), }; - let override_report_validation = - junit::validator::validate(&generated_report, Some(override_report)); + let override_report_validation = junit::validator::validate( + &generated_report, + Some(override_report), + &BundleRepo::default(), + ); pretty_assertions::assert_eq!( override_report_validation.all_issues(), &[ @@ -464,8 +485,11 @@ fn validate_test_runner_report_overrides_timestamp() { .checked_sub_signed(TimeDelta::minutes(1)) .unwrap(), }; - let override_report_validation = - junit::validator::validate(&generated_report, Some(override_report)); + let override_report_validation = junit::validator::validate( + &generated_report, + Some(override_report), + &BundleRepo::default(), + ); pretty_assertions::assert_eq!( override_report_validation.test_runner_report.issues(), &[TestRunnerReportValidationIssue::SubOptimal( @@ -487,8 +511,11 @@ fn validate_test_runner_report_overrides_timestamp() { .checked_add_signed(TimeDelta::minutes(1)) .unwrap(), }; - let override_report_validation = - junit::validator::validate(&generated_report, Some(override_report)); + let override_report_validation = junit::validator::validate( + &generated_report, + Some(override_report), + &BundleRepo::default(), + ); pretty_assertions::assert_eq!( override_report_validation.all_issues(), &[], @@ -498,7 +525,8 @@ fn validate_test_runner_report_overrides_timestamp() { } { - let report_validation = junit::validator::validate(&generated_report, None); + let report_validation = + junit::validator::validate(&generated_report, None, &BundleRepo::default()); pretty_assertions::assert_eq!( report_validation.all_issues(), &[JunitValidationIssueType::Report( @@ -536,8 +564,11 @@ fn validate_test_runner_report_overrides_timestamp() { test_case.timestamp = Some(test_case_timestamp); }); }); - let override_report_validation = - junit::validator::validate(&generated_report, Some(override_report)); + let override_report_validation = junit::validator::validate( + &generated_report, + Some(override_report), + &BundleRepo::default(), + ); pretty_assertions::assert_eq!( override_report_validation.all_issues(), &[ @@ -588,7 +619,8 @@ fn parse_round_trip_and_validate_fuzzed() { for (index, generated_report) in generated_reports.iter().enumerate() { let serialized_generated_report = serialize_report(generated_report); let first_parsed_report = parse_report(&serialized_generated_report); - let report_validation = junit::validator::validate(&first_parsed_report, None); + let report_validation = + junit::validator::validate(&first_parsed_report, None, &BundleRepo::default()); assert_eq!( report_validation.max_level(), @@ -619,7 +651,8 @@ fn parse_round_trip_and_validate_fuzzed() { fn parse_without_testsuites_element() { let options = new_mock_junit_options(1, Some(1), Some(1), true); let mut jm = JunitMock::new(options); - let reports = jm.generate_reports(); + let tmp_dir: Option = None; + let reports = jm.generate_reports(&tmp_dir); let tempdir = TempDir::new().unwrap(); let xml_path = jm diff --git a/junit-mock/src/lib.rs b/junit-mock/src/lib.rs index 5ab74d3c..b7f8464f 100644 --- a/junit-mock/src/lib.rs +++ b/junit-mock/src/lib.rs @@ -254,7 +254,7 @@ impl JunitMock { self.timestamp += duration; } - pub fn generate_reports(&mut self) -> Vec { + pub fn generate_reports>(&mut self, tmp_dir: &Option) -> Vec { self.timestamp = self .options .global @@ -280,7 +280,7 @@ impl JunitMock { let mut report = Report::new(report_name); report.set_timestamp(self.timestamp); self.total_duration = Duration::new(0, 0); - report.add_test_suites(self.generate_test_suites()); + report.add_test_suites(self.generate_test_suites(tmp_dir)); report.set_time(self.total_duration); let duration = self.fake_duration(self.options.report.report_duration_range.clone()); @@ -337,7 +337,7 @@ impl JunitMock { Ok(()) } - fn generate_test_suites(&mut self) -> Vec { + fn generate_test_suites>(&mut self, tmp_dir: &Option) -> Vec { self.options .test_suite .test_suite_names @@ -357,7 +357,7 @@ impl JunitMock { let mut test_suite = TestSuite::new(test_suite_name); test_suite.set_timestamp(self.timestamp); let last_duration = self.total_duration; - test_suite.add_test_cases(self.generate_test_cases()); + test_suite.add_test_cases(self.generate_test_cases(tmp_dir)); test_suite.set_time(self.total_duration - last_duration); if self.rand_bool(self.options.test_suite.test_suite_sys_out_percentage) { test_suite.set_system_out(self.fake_paragraphs()); @@ -370,7 +370,7 @@ impl JunitMock { .collect() } - fn generate_test_cases(&mut self) -> Vec { + fn generate_test_cases>(&mut self, tmp_dir: &Option) -> Vec { let classnames = self .options .test_case @@ -383,7 +383,7 @@ impl JunitMock { }) .unwrap_or_else(|| { (0..self.options.test_case.test_case_random_count) - .map(|_| fake::faker::filesystem::en::DirPath().fake_with_rng(&mut self.rng)) + .map(|_| fake::faker::lorem::en::Word().fake_with_rng(&mut self.rng)) .collect() }); @@ -411,8 +411,21 @@ impl JunitMock { let is_skipped = matches!(&test_case_status, TestCaseStatus::Skipped { .. }); let mut test_case = TestCase::new(test_case_name, test_case_status); - let file: String = - fake::faker::filesystem::en::FilePath().fake_with_rng(&mut self.rng); + let file: String = if let Some(parent_dir) = tmp_dir { + let path = parent_dir + .as_ref() + .join::(fake::faker::lorem::en::Word().fake_with_rng(&mut self.rng)) + .join::( + fake::faker::filesystem::en::FileName().fake_with_rng(&mut self.rng), + ); + std::fs::create_dir_all(path.clone().parent().unwrap()).unwrap(); + if !path.exists() { + std::fs::File::create_new(path.clone()).unwrap(); + } + String::from(path.clone().as_os_str().to_str().unwrap()) + } else { + fake::faker::filesystem::en::FilePath().fake_with_rng(&mut self.rng) + }; test_case.extra.insert("file".into(), file.into()); test_case.set_classname(format!("{test_case_classname}/{test_case_name}")); test_case.set_assertions(self.rng.gen_range(1..10)); diff --git a/junit-mock/src/main.rs b/junit-mock/src/main.rs index d8c2fbd6..3d7dc617 100644 --- a/junit-mock/src/main.rs +++ b/junit-mock/src/main.rs @@ -20,7 +20,8 @@ fn main() -> Result<()> { let mut jm = JunitMock::new(options); println!("Using seed `{}` to generate random data.", jm.get_seed()); - let reports = jm.generate_reports(); + let tmp_dir: Option = None; + let reports = jm.generate_reports(&tmp_dir); jm.write_reports_to_file(directory, &reports)?; diff --git a/proto/proto/test_context.proto b/proto/proto/test_context.proto index 6930eb84..182b80b5 100644 --- a/proto/proto/test_context.proto +++ b/proto/proto/test_context.proto @@ -29,6 +29,7 @@ message TestCaseRun { string status_output_message = 11; bool is_quarantined = 12; repeated CodeOwner codeowners = 13; + string detected_file = 14; } message UploaderMetadata {