diff --git a/Cargo.lock b/Cargo.lock index cbdaaf7d..4676289a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -931,6 +931,7 @@ dependencies = [ "tempfile", "test_utils", "thiserror 1.0.69", + "tsify-next", "uuid", "wasm-bindgen", ] diff --git a/bundle/src/files.rs b/bundle/src/files.rs index f082d3dc..d727821d 100644 --- a/bundle/src/files.rs +++ b/bundle/src/files.rs @@ -2,6 +2,7 @@ use std::{format, time::SystemTime}; use codeowners::{CodeOwners, Owners, OwnersOfPath}; use constants::ALLOW_LIST; +use context::junit::junit_path::{JunitReportFileWithStatus, JunitReportStatus}; #[cfg(feature = "pyo3")] use pyo3::prelude::*; #[cfg(feature = "pyo3")] @@ -39,6 +40,8 @@ pub struct FileSet { pub file_set_type: FileSetType, pub files: Vec, pub glob: String, + /// Added in v0.6.11. Populated when parsing from BEP, not from junit globs + pub test_runner_status: Option, } impl FileSet { @@ -47,12 +50,16 @@ impl FileSet { /// pub fn scan_from_glob( repo_root: &str, - glob_path: String, + glob_with_status: JunitReportFileWithStatus, file_counter: &mut FileSetCounter, team: Option, codeowners: &Option, start: Option, ) -> anyhow::Result { + let JunitReportFileWithStatus { + junit_path: glob_path, + status: test_runner_status, + } = glob_with_status; let path_to_scan = if !std::path::Path::new(&glob_path).is_absolute() { std::path::Path::new(repo_root) .join(&glob_path) @@ -147,6 +154,7 @@ impl FileSet { file_set_type: FileSetType::Junit, files, glob: glob_path, + test_runner_status, }) } } diff --git a/bundle/src/types.rs b/bundle/src/types.rs index edd85347..5f2ad136 100644 --- a/bundle/src/types.rs +++ b/bundle/src/types.rs @@ -31,6 +31,7 @@ pub struct Test { pub class_name: Option, pub file: Option, pub id: String, + /// Added in v0.6.9 pub timestamp_millis: Option, } diff --git a/cli-tests/src/upload.rs b/cli-tests/src/upload.rs index 81ff98a3..a3572d98 100644 --- a/cli-tests/src/upload.rs +++ b/cli-tests/src/upload.rs @@ -252,6 +252,61 @@ async fn upload_bundle_success_status_code() { let temp_dir = tempdir().unwrap(); generate_mock_git_repo(&temp_dir); let test_bep_path = get_test_file_path("test_fixtures/bep_retries"); + // The test cases need not match up or have timestamps, so long as there is a testSummary + // That indicates a flake or pass + let uri_fail = format!( + "file://{}", + get_test_file_path("../cli/test_fixtures/junit1_fail.xml") + ); + let uri_pass = format!( + "file://{}", + get_test_file_path("../cli/test_fixtures/junit0_pass.xml") + ); + + let bep_content = fs::read_to_string(&test_bep_path) + .unwrap() + .replace("${URI_FAIL}", &uri_fail) + .replace("${URI_PASS}", &uri_pass); + let bep_path = temp_dir.path().join("bep.json"); + fs::write(&bep_path, bep_content).unwrap(); + + let state = MockServerBuilder::new().spawn_mock_server().await; + + let args = &[ + "upload", + "--bazel-bep-path", + "./bep.json", + "--org-url-slug", + "test-org", + "--token", + "test-token", + ]; + + // Even though the junits contain failures, they contain retries that succeeded, + // so the upload command should have a successful exit code + let assert = Command::new(CARGO_RUN.path()) + .current_dir(&temp_dir) + .env("TRUNK_PUBLIC_API_ADDRESS", &state.host) + .env("CI", "1") + .env("GITHUB_JOB", "test-job") + .args(args) + .assert() + .code(0) + .success(); + + // No quarantine request + let requests = state.requests.lock().unwrap().clone(); + assert_eq!(requests.len(), 4); + + // HINT: View CLI output with `cargo test -- --nocapture` + println!("{assert}"); +} + +#[tokio::test(flavor = "multi_thread")] +async fn upload_bundle_success_timestamp_status_code() { + let temp_dir = tempdir().unwrap(); + generate_mock_git_repo(&temp_dir); + let test_bep_path = get_test_file_path("test_fixtures/bep_retries_timestamp"); let uri_fail = format!( "file://{}", get_test_file_path("../cli/test_fixtures/junit0_fail.xml") diff --git a/cli-tests/src/utils.rs b/cli-tests/src/utils.rs index 94b070af..e11b5c4a 100644 --- a/cli-tests/src/utils.rs +++ b/cli-tests/src/utils.rs @@ -1,5 +1,8 @@ use bazel_bep::types::build_event_stream::{ - build_event::Payload, file::File::Uri, BuildEvent, File, TestResult, + build_event::Payload, + build_event_id::{Id, TestResultId}, + file::File::Uri, + BuildEvent, BuildEventId, File, TestResult, }; use chrono::{TimeDelta, Utc}; use escargot::{CargoBuild, CargoRun}; @@ -54,6 +57,12 @@ pub fn generate_mock_bazel_bep>(directory: T) { ..Default::default() }]; build_event.payload = Some(Payload::TestResult(payload)); + build_event.id = Some(BuildEventId { + id: Some(Id::TestResult(TestResultId { + label: "//path:test".to_string(), + ..Default::default() + })), + }); build_event }) .collect(); diff --git a/cli-tests/test_fixtures/bep_retries b/cli-tests/test_fixtures/bep_retries index 0955a16c..fc30978c 100644 --- a/cli-tests/test_fixtures/bep_retries +++ b/cli-tests/test_fixtures/bep_retries @@ -78,4 +78,36 @@ "testAttemptStart": "2024-12-10T06:17:24.011Z", "testAttemptDuration": "0.044s" } +} +{ + "id": { + "testSummary": { + "label": "//trunk/hello_world/cc:hello_test", + "configuration": { + "id": "2136e65c67d9ba6b2652accfbf6533486c16692a484f4dc7ea7756de0edc13a6" + } + } + }, + "testSummary": { + "totalRunCount": 2, + "passed": [ + { + "uri": "file:///repos/trunk/trunk/hello_world/cc/hello_test/test.log" + } + ], + "failed": [ + { + "uri": "file:///repos/trunk/trunk/hello_world/cc/hello_test/test_attempts/attempt_1.log" + } + ], + "overallStatus": "FLAKY", + "firstStartTimeMillis": "1734408655425", + "lastStopTimeMillis": "1734408655466", + "totalRunDurationMillis": "41", + "runCount": 1, + "totalRunDuration": "0.041s", + "firstStartTime": "2024-12-17T04:10:55.425Z", + "lastStopTime": "2024-12-17T04:10:55.466Z", + "attemptCount": 2 + } } \ No newline at end of file diff --git a/cli-tests/test_fixtures/bep_retries_timestamp b/cli-tests/test_fixtures/bep_retries_timestamp new file mode 100644 index 00000000..0955a16c --- /dev/null +++ b/cli-tests/test_fixtures/bep_retries_timestamp @@ -0,0 +1,81 @@ +{ + "id": { + "testResult": { + "label": "//trunk/hello_world/cc:hello_test", + "run": 1, + "shard": 1, + "attempt": 1, + "configuration": { + "id": "2136e65c67d9ba6b2652accfbf6533486c16692a484f4dc7ea7756de0edc13a6" + } + } + }, + "children": [ + { + "testResult": { + "label": "//trunk/hello_world/cc:hello_test", + "run": 1, + "shard": 1, + "attempt": 2, + "configuration": { + "id": "2136e65c67d9ba6b2652accfbf6533486c16692a484f4dc7ea7756de0edc13a6" + } + } + } + ], + "testResult": { + "testActionOutput": [ + { + "name": "test.xml", + "uri": "${URI_FAIL}" + } + ], + "testAttemptDurationMillis": "43", + "status": "FAILED", + "testAttemptStartMillisEpoch": "1733811443963", + "executionInfo": { + "strategy": "linux-sandbox", + "timingBreakdown": { + "child": [], + "name": "totalTime", + "time": "0.043s" + } + }, + "testAttemptStart": "2024-12-10T06:17:23.963Z", + "testAttemptDuration": "0.043s" + } +} +{ + "id": { + "testResult": { + "label": "//trunk/hello_world/cc:hello_test", + "run": 1, + "shard": 1, + "attempt": 2, + "configuration": { + "id": "2136e65c67d9ba6b2652accfbf6533486c16692a484f4dc7ea7756de0edc13a6" + } + } + }, + "testResult": { + "testActionOutput": [ + { + "name": "test.xml", + "uri": "${URI_PASS}" + } + ], + "testAttemptDurationMillis": "44", + "status": "PASSED", + "testAttemptStartMillisEpoch": "1733811444011", + "executionInfo": { + "strategy": "linux-sandbox", + "timingBreakdown": { + "child": [], + "name": "totalTime", + "time": "0.044s" + } + }, + "testAttemptStart": "2024-12-10T06:17:24.011Z", + "testAttemptDuration": "0.044s" + } +} \ No newline at end of file diff --git a/cli/src/main.rs b/cli/src/main.rs index d88df7cf..3b2799d1 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -5,7 +5,10 @@ use bundle::RunResult; use clap::{Args, Parser, Subcommand}; use codeowners::CodeOwners; use constants::{EXIT_FAILURE, SENTRY_DSN}; -use context::{bazel_bep::parser::BazelBepParser, repo::BundleRepo}; +use context::{ + bazel_bep::parser::BazelBepParser, junit::junit_path::JunitReportFileWithStatus, + repo::BundleRepo, +}; use trunk_analytics_cli::{ api_client::ApiClient, print::print_bep_results, @@ -235,7 +238,10 @@ async fn run(cli: Cli) -> anyhow::Result { print_bep_results(&bep_result); bep_result.uncached_xml_files() } - None => junit_paths, + None => junit_paths + .into_iter() + .map(JunitReportFileWithStatus::from) + .collect(), }; validate(junit_file_paths, show_warnings, codeowners_path).await } diff --git a/cli/src/runner.rs b/cli/src/runner.rs index d8308481..65f692ca 100644 --- a/cli/src/runner.rs +++ b/cli/src/runner.rs @@ -9,7 +9,14 @@ use bundle::{ }; use codeowners::CodeOwners; use constants::{EXIT_FAILURE, EXIT_SUCCESS}; -use context::{bazel_bep::parser::BazelBepParser, junit::parser::JunitParser, repo::BundleRepo}; +use context::{ + bazel_bep::parser::BazelBepParser, + junit::{ + junit_path::{JunitReportFileWithStatus, JunitReportStatus}, + parser::JunitParser, + }, + repo::BundleRepo, +}; use crate::{api_client::ApiClient, print::print_bep_results}; @@ -32,7 +39,10 @@ pub async fn run_test_command( log::info!("Command exit code: {}", exit_code); let output_paths = match junit_spec { - JunitSpec::Paths(paths) => paths, + JunitSpec::Paths(paths) => paths + .into_iter() + .map(JunitReportFileWithStatus::from) + .collect(), JunitSpec::BazelBep(bep_path) => { let mut parser = BazelBepParser::new(bep_path); let bep_result = parser.parse()?; @@ -86,7 +96,7 @@ async fn run_test_and_get_exit_code(command: &String, args: Vec<&String>) -> any pub fn build_filesets( repo_root: &str, - junit_paths: &[String], + junit_paths: &[JunitReportFileWithStatus], team: Option, codeowners: &Option, exec_start: Option, @@ -94,10 +104,10 @@ pub fn build_filesets( let mut file_counter = FileSetCounter::default(); let mut file_sets = junit_paths .iter() - .map(|path| { + .map(|junit_wrapper| { FileSet::scan_from_glob( repo_root, - path.to_string(), + junit_wrapper.clone(), &mut file_counter, team.clone(), codeowners, @@ -110,15 +120,18 @@ pub fn build_filesets( if file_counter.get_count() == 0 { file_sets = junit_paths .iter() - .map(|path| { - let mut path = path.clone(); + .map(|junit_wrapper| { + let mut path = junit_wrapper.junit_path.clone(); if !path.ends_with('/') { path.push('/'); } path.push_str("**/*.xml"); FileSet::scan_from_glob( repo_root, - path.to_string(), + JunitReportFileWithStatus { + junit_path: path, + status: junit_wrapper.status.clone(), + }, &mut file_counter, team.clone(), codeowners, @@ -168,6 +181,12 @@ pub async fn extract_failed_tests( let mut successes: HashMap = HashMap::new(); for file_set in file_sets { + if let Some(test_runner_status) = &file_set.test_runner_status { + // TODO(TRUNK-13911): We should populate the status for all junits, regardless of the presence of a test runner status. + if test_runner_status != &JunitReportStatus::Failed { + continue; + } + } for file in &file_set.files { let file = match std::fs::File::open(&file.original_path) { Ok(file) => file, @@ -347,6 +366,7 @@ mod tests { }, ], glob: String::from("**/*.xml"), + test_runner_status: None, }]; let retried_failures = @@ -369,6 +389,7 @@ mod tests { }, ], glob: String::from("**/*.xml"), + test_runner_status: None, }]; let retried_failures = @@ -391,6 +412,7 @@ mod tests { }, ], glob: String::from("**/*.xml"), + test_runner_status: None, }]; let mut multi_failures = @@ -424,6 +446,7 @@ mod tests { }, ], glob: String::from("**/*.xml"), + test_runner_status: None, }]; let some_failures = @@ -431,4 +454,43 @@ mod tests { assert_eq!(some_failures.len(), 1); assert_eq!(some_failures[0].name, "Goodbye"); } + + #[tokio::test(start_paused = true)] + async fn test_extract_multi_failed_tests_with_runner_status() { + let file_sets = vec![ + FileSet { + file_set_type: FileSetType::Junit, + files: vec![BundledFile { + original_path: get_test_file_path(JUNIT1_FAIL), + ..BundledFile::default() + }], + glob: String::from("1/*.xml"), + test_runner_status: Some(JunitReportStatus::Passed), + }, + FileSet { + file_set_type: FileSetType::Junit, + files: vec![BundledFile { + original_path: get_test_file_path(JUNIT1_FAIL), + ..BundledFile::default() + }], + glob: String::from("2/*.xml"), + test_runner_status: Some(JunitReportStatus::Flaky), + }, + FileSet { + file_set_type: FileSetType::Junit, + files: vec![BundledFile { + original_path: get_test_file_path(JUNIT0_FAIL), + ..BundledFile::default() + }], + glob: String::from("3/*.xml"), + test_runner_status: Some(JunitReportStatus::Failed), + }, + ]; + + let mut multi_failures = + extract_failed_tests(&BundleRepo::default(), ORG_SLUG, &file_sets).await; + multi_failures.sort_by(|a, b| a.name.cmp(&b.name)); + assert_eq!(multi_failures.len(), 1); + assert_eq!(multi_failures[0].name, "Hello"); + } } diff --git a/cli/src/upload.rs b/cli/src/upload.rs index f86de44f..95e13a6d 100644 --- a/cli/src/upload.rs +++ b/cli/src/upload.rs @@ -17,10 +17,12 @@ use bundle::{ use codeowners::CodeOwners; use constants::{EXIT_FAILURE, EXIT_SUCCESS}; #[cfg(target_os = "macos")] +use context::junit::junit_path::JunitReportStatus; +#[cfg(target_os = "macos")] use context::repo::RepoUrlParts; use context::{ bazel_bep::parser::{BazelBepParser, BepParseResult}, - junit::parser::JunitParser, + junit::{junit_path::JunitReportFileWithStatus, parser::JunitParser}, repo::BundleRepo, }; @@ -121,7 +123,7 @@ pub async fn run_upload( exec_start: Option, ) -> anyhow::Result { let UploadArgs { - mut junit_paths, + junit_paths, #[cfg(target_os = "macos")] xcresult_path, bazel_bep_path, @@ -140,6 +142,10 @@ pub async fn run_upload( team, codeowners_path, } = upload_args; + let mut junit_path_wrappers = junit_paths + .into_iter() + .map(JunitReportFileWithStatus::from) + .collect(); let repo = BundleRepo::new( repo_root, @@ -163,7 +169,7 @@ pub async fn run_upload( let mut parser = BazelBepParser::new(bazel_bep_path); let bep_parse_result = parser.parse()?; print_bep_results(&bep_parse_result); - junit_paths = bep_parse_result.uncached_xml_files(); + junit_path_wrappers = bep_parse_result.uncached_xml_files(); bep_result = Some(bep_parse_result); } @@ -174,19 +180,19 @@ pub async fn run_upload( { let temp_paths = handle_xcresult(&junit_temp_dir, xcresult_path, &repo.repo, &org_url_slug)?; - junit_paths = [junit_paths.as_slice(), temp_paths.as_slice()].concat(); - if junit_paths.is_empty() && !allow_empty_test_results { + junit_path_wrappers = [junit_path_wrappers.as_slice(), temp_paths.as_slice()].concat(); + if junit_path_wrappers.is_empty() && !allow_empty_test_results { return Err(anyhow::anyhow!( "No tests found in the provided XCResult path." )); - } else if junit_paths.is_empty() && allow_empty_test_results { + } else if junit_path_wrappers.is_empty() && allow_empty_test_results { log::warn!("No tests found in the provided XCResult path."); } } let (file_sets, file_counter) = build_filesets( &repo.repo_root, - &junit_paths, + &junit_path_wrappers, team.clone(), &codeowners, exec_start, @@ -295,7 +301,7 @@ pub async fn run_upload( if file_counter.get_count() == 0 { log::warn!( "No JUnit files found to pack and upload using globs: {:?}", - junit_paths + junit_path_wrappers.iter().map(|j| &j.junit_path) ); } @@ -370,7 +376,7 @@ fn handle_xcresult( xcresult_path: Option, repo: &RepoUrlParts, org_url_slug: &str, -) -> Result, anyhow::Error> { +) -> Result, anyhow::Error> { let mut temp_paths = Vec::new(); if let Some(xcresult_path) = xcresult_path { let xcresult = XCResult::new(xcresult_path, repo, org_url_slug.to_string()); @@ -389,7 +395,10 @@ fn handle_xcresult( .map_err(|e| anyhow::anyhow!("Failed to write junit file: {}", e))?; let junit_temp_path_str = junit_temp_path.to_str(); if let Some(junit_temp_path_string) = junit_temp_path_str { - temp_paths.push(junit_temp_path_string.to_string()); + temp_paths.push(JunitReportFileWithStatus { + junit_path: junit_temp_path_string.to_string(), + status: None, + }); } else { return Err(anyhow::anyhow!( "Failed to convert junit temp path to string." diff --git a/cli/src/validate.rs b/cli/src/validate.rs index 30cc1926..ed48e243 100644 --- a/cli/src/validate.rs +++ b/cli/src/validate.rs @@ -7,6 +7,7 @@ use colored::{ColoredString, Colorize}; use console::Emoji; use constants::{EXIT_FAILURE, EXIT_SUCCESS}; use context::junit::{ + junit_path::JunitReportFileWithStatus, parser::{JunitParseError, JunitParser}, validator::{ validate as validate_report, JunitReportValidation, JunitReportValidationFlatIssue, @@ -21,7 +22,7 @@ type JunitFileToReportAndErrors = BTreeMap, Vec< type JunitFileToValidation = BTreeMap>; pub async fn validate( - junit_paths: Vec, + junit_paths: Vec, show_warnings: bool, codeowners_path: Option, ) -> anyhow::Result { diff --git a/context-js/tests/parse_compressed_bundle.test.ts b/context-js/tests/parse_compressed_bundle.test.ts index 2c2e471a..b323f4e0 100644 --- a/context-js/tests/parse_compressed_bundle.test.ts +++ b/context-js/tests/parse_compressed_bundle.test.ts @@ -35,6 +35,7 @@ const generateBundleMeta = (): TestBundleMeta => ({ }, ], glob: "**/*.xml", + test_runner_status: null, }, ], org: faker.company.name(), diff --git a/context/Cargo.toml b/context/Cargo.toml index 47b0e54e..cf43fede 100644 --- a/context/Cargo.toml +++ b/context/Cargo.toml @@ -28,6 +28,7 @@ serde = { version = "1.0.215", default-features = false, features = ["derive"] } serde_json = "1.0.133" speedate = "0.14.4" thiserror = "1.0.63" +tsify-next = { version = "0.5.4", optional = true } uuid = { version = "1.10.0", features = ["v5"] } wasm-bindgen = { version = "0.2.95", optional = true } magnus = { version = "0.7.1", optional = true, default-features = false } @@ -47,5 +48,5 @@ default = ["git-access"] git-access = ["dep:gix", "dep:openssl"] bindings = [] pyo3 = ["bindings", "dep:pyo3", "dep:pyo3-stub-gen"] -wasm = ["bindings", "dep:wasm-bindgen", "dep:js-sys"] +wasm = ["bindings", "dep:wasm-bindgen", "dep:js-sys", "dep:tsify-next"] ruby = ["bindings", "dep:magnus"] diff --git a/context/src/bazel_bep/parser.rs b/context/src/bazel_bep/parser.rs index 160606a9..7cad7fb0 100644 --- a/context/src/bazel_bep/parser.rs +++ b/context/src/bazel_bep/parser.rs @@ -1,13 +1,17 @@ -use std::path::PathBuf; - +use crate::junit::junit_path::{JunitReportFileWithStatus, JunitReportStatus}; use anyhow::Ok; -use bazel_bep::types::build_event_stream::{build_event::Payload, file::File::Uri, BuildEvent}; +use bazel_bep::types::build_event_stream::{ + build_event::Payload, build_event_id::Id, file::File::Uri, BuildEvent, +}; use serde_json::Deserializer; +use std::{collections::HashMap, path::PathBuf}; #[derive(Debug, Clone, Default)] pub struct TestResult { + pub label: String, pub cached: bool, pub xml_files: Vec, + pub summary_status: Option, } const FILE_URI_PREFIX: &str = "file://"; @@ -34,14 +38,22 @@ impl BepParseResult { (xml_count, cached_xml_count) } - pub fn uncached_xml_files(&self) -> Vec { + pub fn uncached_xml_files(&self) -> Vec { self.test_results .iter() .filter_map(|r| { if r.cached { return None; } - Some(r.xml_files.clone()) + Some( + r.xml_files + .iter() + .map(|f| JunitReportFileWithStatus { + junit_path: f.clone(), + status: r.summary_status.clone(), + }) + .collect::>(), + ) }) .flatten() .collect() @@ -68,64 +80,99 @@ impl BazelBepParser { let file = std::fs::File::open(&self.bazel_bep_path)?; let reader = std::io::BufReader::new(file); - let (errors, test_results, bep_test_events) = Deserializer::from_reader(reader) - .into_iter::() - .fold( - ( - Vec::::new(), - Vec::::new(), - Vec::::new(), - ), - |(mut errors, mut test_results, mut bep_test_events), parse_event| { - match parse_event { - Result::Err(ref err) => { - errors.push(format!("Error parsing build event: {}", err)); - } - Result::Ok(build_event) => { - if let Some(Payload::TestResult(test_result)) = &build_event.payload { - let xml_files: Vec = test_result - .test_action_output - .iter() - .filter_map(|action_output| { - if action_output.name.ends_with(".xml") { - action_output.file.clone().and_then(|f| { - if let Uri(uri) = f { - Some( - uri.strip_prefix(FILE_URI_PREFIX) - .unwrap_or(&uri) - .to_string(), - ) + let (errors, test_results, summary_statuses, bep_test_events) = + Deserializer::from_reader(reader) + .into_iter::() + .fold( + ( + Vec::::new(), + Vec::::new(), + HashMap::::new(), + Vec::::new(), + ), + |(mut errors, mut test_results, mut summary_statuses, mut bep_test_events), + parse_event| { + match parse_event { + Result::Err(ref err) => { + errors.push(format!("Error parsing build event: {}", err)); + } + Result::Ok(build_event) => { + let payload = &build_event.payload; + let id = build_event.id.clone().and_then(|id| id.id); + match (payload, id) { + ( + Some(Payload::TestSummary(test_summary)), + Some(Id::TestSummary(id)), + ) => { + if let Result::Ok(status) = JunitReportStatus::try_from( + test_summary.overall_status(), + ) { + summary_statuses.insert(id.label.clone(), status); + } + bep_test_events.push(build_event); + } + ( + Some(Payload::TestResult(test_result)), + Some(Id::TestResult(id)), + ) => { + let xml_files = test_result + .test_action_output + .iter() + .filter_map(|action_output| { + if action_output.name.ends_with(".xml") { + action_output.file.clone().and_then(|f| { + if let Uri(uri) = f { + Some( + uri.strip_prefix(FILE_URI_PREFIX) + .unwrap_or(&uri) + .to_string(), + ) + } else { + None + } + }) } else { None } }) + .collect(); + + let cached = if let Some(execution_info) = + &test_result.execution_info + { + execution_info.cached_remotely + || test_result.cached_locally } else { - None - } - }) - .collect(); - - let cached = - if let Some(execution_info) = &test_result.execution_info { - execution_info.cached_remotely || test_result.cached_locally - } else { - test_result.cached_locally - }; - - bep_test_events.push(build_event); - test_results.push(TestResult { cached, xml_files }); + test_result.cached_locally + }; + + test_results.push(TestResult { + label: id.label.clone(), + cached, + xml_files, + summary_status: None, + }); + bep_test_events.push(build_event); + } + _ => {} + } } } - } - (errors, test_results, bep_test_events) - }, - ); + (errors, test_results, summary_statuses, bep_test_events) + }, + ); Ok(BepParseResult { bep_test_events, errors, - test_results, + test_results: test_results + .into_iter() + .map(|test_result| TestResult { + summary_status: summary_statuses.get(&test_result.label).cloned(), + ..test_result + }) + .collect(), }) } } @@ -138,6 +185,7 @@ mod tests { const SIMPLE_EXAMPLE: &str = "test_fixtures/bep_example"; const EMPTY_EXAMPLE: &str = "test_fixtures/bep_empty"; const PARTIAL_EXAMPLE: &str = "test_fixtures/bep_partially_valid"; + const FLAKY_SUMMARY_EXAMPLE: &str = "test_fixtures/bep_flaky_summary"; #[test] fn test_parse_simple_bep() { @@ -148,7 +196,10 @@ mod tests { let empty_vec: Vec = Vec::new(); assert_eq!( parse_result.uncached_xml_files(), - vec!["/tmp/hello_test/test.xml"] + vec![JunitReportFileWithStatus { + junit_path: "/tmp/hello_test/test.xml".to_string(), + status: None + }] ); assert_eq!(parse_result.xml_file_counts(), (1, 0)); assert_eq!(*parse_result.errors, empty_vec); @@ -160,10 +211,11 @@ mod tests { let mut parser = BazelBepParser::new(input_file); let parse_result = parser.parse().unwrap(); - let empty_vec: Vec = Vec::new(); - assert_eq!(parse_result.uncached_xml_files(), empty_vec); + let empty_xml_vec: Vec = Vec::new(); + let empty_errors_vec: Vec = Vec::new(); + assert_eq!(parse_result.uncached_xml_files(), empty_xml_vec); assert_eq!(parse_result.xml_file_counts(), (0, 0)); - assert_eq!(*parse_result.errors, empty_vec); + assert_eq!(*parse_result.errors, empty_errors_vec); } #[test] @@ -174,7 +226,16 @@ mod tests { assert_eq!( parse_result.uncached_xml_files(), - vec!["/tmp/hello_test/test.xml", "/tmp/client_test/test.xml"] + vec![ + JunitReportFileWithStatus { + junit_path: "/tmp/hello_test/test.xml".to_string(), + status: Some(JunitReportStatus::Passed) + }, + JunitReportFileWithStatus { + junit_path: "/tmp/client_test/test.xml".to_string(), + status: Some(JunitReportStatus::Passed) + } + ] ); assert_eq!(parse_result.xml_file_counts(), (3, 1)); assert_eq!( @@ -182,4 +243,34 @@ mod tests { vec!["Error parsing build event: EOF while parsing a value at line 108 column 0"] ); } + + #[test] + fn test_parse_flaky_summary_bep() { + let input_file = get_test_file_path(FLAKY_SUMMARY_EXAMPLE); + let mut parser = BazelBepParser::new(input_file); + let parse_result = parser.parse().unwrap(); + + assert_eq!( + parse_result.uncached_xml_files(), + vec![ + JunitReportFileWithStatus { + junit_path: "/tmp/hello_test/test_attempts/attempt_1.xml".to_string(), + status: Some(JunitReportStatus::Flaky) + }, + JunitReportFileWithStatus { + junit_path: "/tmp/hello_test/test_attempts/attempt_2.xml".to_string(), + status: Some(JunitReportStatus::Flaky) + }, + JunitReportFileWithStatus { + junit_path: "/tmp/hello_test/test.xml".to_string(), + status: Some(JunitReportStatus::Flaky) + }, + JunitReportFileWithStatus { + junit_path: "/tmp/client_test/test.xml".to_string(), + status: Some(JunitReportStatus::Failed) + } + ] + ); + assert_eq!(parse_result.xml_file_counts(), (4, 0)); + } } diff --git a/context/src/junit/junit_path.rs b/context/src/junit/junit_path.rs new file mode 100644 index 00000000..8956d1fa --- /dev/null +++ b/context/src/junit/junit_path.rs @@ -0,0 +1,54 @@ +#[cfg(feature = "pyo3")] +use pyo3::prelude::*; +#[cfg(feature = "pyo3")] +use pyo3_stub_gen::derive::gen_stub_pyclass_enum; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "wasm")] +use tsify_next::Tsify; +#[cfg(feature = "wasm")] +use wasm_bindgen::prelude::*; + +use bazel_bep::types::build_event_stream::TestStatus; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] +#[cfg_attr(feature = "pyo3", gen_stub_pyclass_enum, pyclass(eq, eq_int))] +#[cfg_attr(feature = "wasm", derive(Tsify))] +pub enum JunitReportStatus { + #[default] + Passed, + Failed, + Flaky, +} + +impl TryFrom for JunitReportStatus { + type Error = (); + + fn try_from(status: TestStatus) -> Result { + match status { + TestStatus::Passed => Ok(JunitReportStatus::Passed), + TestStatus::Failed => Ok(JunitReportStatus::Failed), + TestStatus::Flaky => Ok(JunitReportStatus::Flaky), + _ => Err(()), + } + } +} + +/// Encapsulates the glob path for a junit and, if applicable, the flakiness already +/// assigned by the user's test runner. See bazel_bep/parser.rs for more. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct JunitReportFileWithStatus { + /// Path or glob pattern to the junit file. + pub junit_path: String, + /// Refers to an optional status parsed from the test runner's output, before junits have been parsed. + /// TODO(TRUNK-13911): We should populate the status for all junits, regardless of the presence of a test runner status. + pub status: Option, +} + +impl From for JunitReportFileWithStatus { + fn from(junit_path: String) -> Self { + Self { + junit_path, + status: None, + } + } +} diff --git a/context/src/junit/mod.rs b/context/src/junit/mod.rs index 13185ddd..23716c81 100644 --- a/context/src/junit/mod.rs +++ b/context/src/junit/mod.rs @@ -1,5 +1,6 @@ #[cfg(feature = "bindings")] pub mod bindings; mod date_parser; +pub mod junit_path; pub mod parser; pub mod validator; diff --git a/context/test_fixtures/bep_flaky_summary b/context/test_fixtures/bep_flaky_summary new file mode 100644 index 00000000..e20d2e0c --- /dev/null +++ b/context/test_fixtures/bep_flaky_summary @@ -0,0 +1,39 @@ +{"id":{"started":{}},"children":[{"progress":{}},{"unstructuredCommandLine":{}},{"structuredCommandLine":{"commandLineLabel":"original"}},{"structuredCommandLine":{"commandLineLabel":"canonical"}},{"structuredCommandLine":{"commandLineLabel":"tool"}},{"buildMetadata":{}},{"optionsParsed":{}},{"workspaceStatus":{}},{"pattern":{"pattern":["//trunk/hello_world/cc:hello_test","//trunk/hello_world/cc_grpc:client_test"]}},{"buildFinished":{}}],"started":{"uuid":"337be0c6-0b6a-47fa-a573-19a33412c719","startTimeMillis":"1734408654987","buildToolVersion":"7.4.0","optionsDescription":"","command":"test","workingDirectory":"/repos/trunk","workspaceDirectory":"/repos/trunk","serverPid":"57611","startTime":"2024-12-17T04:10:54.987Z"}} +{"id":{"buildMetadata":{}},"buildMetadata":{}} +{"id":{"unstructuredCommandLine":{}},"unstructuredCommandLine":{"args":[]}} +{"id":{"optionsParsed":{}},"optionsParsed":{"startupOptions":[],"explicitStartupOptions":[],"cmdLine":[],"explicitCmdLine":[],"invocationPolicy":{}}} +{"id":{"structuredCommandLine":{"commandLineLabel":"original"}},"structuredCommandLine":{"commandLineLabel":"original","sections":[]}} +{"id":{"structuredCommandLine":{"commandLineLabel":"canonical"}},"structuredCommandLine":{"commandLineLabel":"canonical","sections":[]}} +{"id":{"structuredCommandLine":{"commandLineLabel":"tool"}},"structuredCommandLine":{}} +{"id":{"pattern":{"pattern":["//trunk/hello_world/cc:hello_test","//trunk/hello_world/cc_grpc:client_test"]}},"children":[{"targetConfigured":{"label":"//trunk/hello_world/cc:hello_test"}},{"targetConfigured":{"label":"//trunk/hello_world/cc_grpc:client_test"}}],"expanded":{}} +{"id":{"progress":{}},"children":[{"progress":{"opaqueCount":1}},{"workspace":{}}],"progress":{"stderr":""}} +{"id":{"workspace":{}},"workspaceInfo":{"localExecRoot":"/tmp/.cache/bazel/deadbeef/execroot/_main"}} +{"id":{"progress":{"opaqueCount":1}},"children":[{"progress":{"opaqueCount":2}},{"configuration":{"id":"2136e65c67d9ba6b2652accfbf6533486c16692a484f4dc7ea7756de0edc13a6"}}],"progress":{}} +{"id":{"configuration":{"id":"2136e65c67d9ba6b2652accfbf6533486c16692a484f4dc7ea7756de0edc13a6"}},"configuration":{"mnemonic":"k8-fastbuild","platformName":"k8","cpu":"k8","makeVariable":{"TRUNK_RELEASE":"0.0.0","METRICS_DATABASE_URL":"","IMAGE_TAG":"null","SENTRY_AUTH_TOKEN":"","TARGET_CPU":"k8","GENDIR":"bazel-out/k8-fastbuild/bin","FLAKY_TESTS_OVERRIDE_REPO_FULL_NAME":"","AWS_PROFILE":"","SERVICE_JEST_ARGS":"--testMatch\u003d**/__tests__/**/*.test.js","ANALYZE":"","BINDIR":"bazel-out/k8-fastbuild/bin","COMPILATION_MODE":"fastbuild","DATABASE_URL":"","absl":"1","SPDLOG_COMPILE_LEVEL":"SPDLOG_LEVEL_TRACE","SENTRY_LOG_LEVEL":"","AWS_ECR_URL":"null","FLAKY_TESTS_OVERRIDE_ORG_SLUG":""}}} +{"id":{"targetConfigured":{"label":"//trunk/hello_world/cc:hello_test"}},"children":[{"targetCompleted":{"label":"//trunk/hello_world/cc:hello_test","configuration":{"id":"2136e65c67d9ba6b2652accfbf6533486c16692a484f4dc7ea7756de0edc13a6"}}}],"configured":{"targetKind":"cc_test rule","testSize":"MEDIUM","tag":["__CC_RULES_MIGRATION_DO_NOT_USE_WILL_BREAK__"]}} +{"id":{"targetConfigured":{"label":"//trunk/hello_world/cc_grpc:client_test"}},"children":[{"targetCompleted":{"label":"//trunk/hello_world/cc_grpc:client_test","configuration":{"id":"2136e65c67d9ba6b2652accfbf6533486c16692a484f4dc7ea7756de0edc13a6"}}}],"configured":{"targetKind":"cc_test rule","testSize":"MEDIUM","tag":["__CC_RULES_MIGRATION_DO_NOT_USE_WILL_BREAK__"]}} +{"id":{"workspaceStatus":{}},"workspaceStatus":{"item":[{"key":"BUILD_EMBED_LABEL"},{"key":"BUILD_HOST","value":"tyler-Precision-3260"},{"key":"BUILD_TIMESTAMP","value":"1734408655"},{"key":"BUILD_USER","value":"tyler"},{"key":"FORMATTED_DATE","value":"2024 Dec 17 04 10 55 Tue"}]}} +{"id":{"progress":{"opaqueCount":2}},"children":[{"progress":{"opaqueCount":3}},{"namedSet":{"id":"0"}}],"progress":{"stderr":""}} +{"id":{"namedSet":{"id":"0"}},"namedSetOfFiles":{"files":[{"name":"trunk/hello_world/cc_grpc/client_test","uri":"file:///tmp/.cache/bazel/deadbeef/execroot/_main/bazel-out/k8-fastbuild/bin/trunk/hello_world/cc_grpc/client_test","pathPrefix":["bazel-out","k8-fastbuild","bin"],"digest":"e666f897f5dd4747c254bee145f40bdc3a7787bdfd9ef87afdb15bab94b8ff73","length":"5536640"}]}} +{"id":{"targetCompleted":{"label":"//trunk/hello_world/cc_grpc:client_test","configuration":{"id":"2136e65c67d9ba6b2652accfbf6533486c16692a484f4dc7ea7756de0edc13a6"}}},"children":[{"testResult":{"label":"//trunk/hello_world/cc_grpc:client_test","run":1,"shard":1,"attempt":1,"configuration":{"id":"2136e65c67d9ba6b2652accfbf6533486c16692a484f4dc7ea7756de0edc13a6"}}},{"testSummary":{"label":"//trunk/hello_world/cc_grpc:client_test","configuration":{"id":"2136e65c67d9ba6b2652accfbf6533486c16692a484f4dc7ea7756de0edc13a6"}}}],"completed":{"success":true,"outputGroup":[{"name":"default","fileSets":[{"id":"0"}]}],"tag":["__CC_RULES_MIGRATION_DO_NOT_USE_WILL_BREAK__","medium","moderate","noflaky","nolocal"],"importantOutput":[{"name":"trunk/hello_world/cc_grpc/client_test","uri":"file:///tmp/.cache/bazel/deadbeef/execroot/_main/bazel-out/k8-fastbuild/bin/trunk/hello_world/cc_grpc/client_test","pathPrefix":["bazel-out","k8-fastbuild","bin"],"digest":"e666f897f5dd4747c254bee145f40bdc3a7787bdfd9ef87afdb15bab94b8ff73","length":"5536640"}],"testTimeoutSeconds":"300","testTimeout":"300s"}} +{"id":{"progress":{"opaqueCount":3}},"children":[{"progress":{"opaqueCount":4}}],"progress":{"stdout":"","stderr":""}} +{"id":{"progress":{"opaqueCount":4}},"children":[{"progress":{"opaqueCount":5}},{"namedSet":{"id":"1"}}],"progress":{"stderr":""}} +{"id":{"namedSet":{"id":"1"}},"namedSetOfFiles":{"files":[{"name":"trunk/hello_world/cc/hello_test","uri":"file:///tmp/.cache/bazel/deadbeef/execroot/_main/bazel-out/k8-fastbuild/bin/trunk/hello_world/cc/hello_test","pathPrefix":["bazel-out","k8-fastbuild","bin"],"digest":"665d899ac16983beb71217dec6958c494a4858add581745e0b72816462c9f793","length":"2692552"}]}} +{"id":{"targetCompleted":{"label":"//trunk/hello_world/cc:hello_test","configuration":{"id":"2136e65c67d9ba6b2652accfbf6533486c16692a484f4dc7ea7756de0edc13a6"}}},"children":[{"testResult":{"label":"//trunk/hello_world/cc:hello_test","run":1,"shard":1,"attempt":1,"configuration":{"id":"2136e65c67d9ba6b2652accfbf6533486c16692a484f4dc7ea7756de0edc13a6"}}},{"testSummary":{"label":"//trunk/hello_world/cc:hello_test","configuration":{"id":"2136e65c67d9ba6b2652accfbf6533486c16692a484f4dc7ea7756de0edc13a6"}}}],"completed":{"success":true,"outputGroup":[{"name":"default","fileSets":[{"id":"1"}]}],"tag":["__CC_RULES_MIGRATION_DO_NOT_USE_WILL_BREAK__","medium","moderate","noflaky","nolocal"],"importantOutput":[{"name":"trunk/hello_world/cc/hello_test","uri":"file:///tmp/.cache/bazel/deadbeef/execroot/_main/bazel-out/k8-fastbuild/bin/trunk/hello_world/cc/hello_test","pathPrefix":["bazel-out","k8-fastbuild","bin"],"digest":"665d899ac16983beb71217dec6958c494a4858add581745e0b72816462c9f793","length":"2692552"}],"testTimeoutSeconds":"300","testTimeout":"300s"}} +{"id":{"testResult":{"label":"//trunk/hello_world/cc:hello_test","run":1,"shard":1,"attempt":1,"configuration":{"id":"2136e65c67d9ba6b2652accfbf6533486c16692a484f4dc7ea7756de0edc13a6"}}},"children":[{"testResult":{"label":"//trunk/hello_world/cc:hello_test","run":1,"shard":1,"attempt":2,"configuration":{"id":"2136e65c67d9ba6b2652accfbf6533486c16692a484f4dc7ea7756de0edc13a6"}}}],"testResult":{"testActionOutput":[{"name":"test.log","uri":"file:///repos/trunk/trunk/hello_world/cc/hello_test/test_attempts/attempt_1.log"},{"name":"test.xml","uri":"file:///tmp/hello_test/test_attempts/attempt_1.xml"}],"testAttemptDurationMillis":"46","status":"FAILED","testAttemptStartMillisEpoch":"1734408655311","executionInfo":{"strategy":"linux-sandbox","timingBreakdown":{"child":[{"name":"parseTime","time":"0s"},{"name":"fetchTime","time":"0s"},{"name":"queueTime","time":"0s"},{"name":"uploadTime","time":"0s"},{"name":"setupTime","time":"0s"},{"name":"executionWallTime","time":"0.046s"},{"name":"processOutputsTime","time":"0s"},{"name":"networkTime","time":"0s"}],"name":"totalTime","time":"0.046s"}},"testAttemptStart":"2024-12-17T04:10:55.311Z","testAttemptDuration":"0.046s"}} +{"id":{"testResult":{"label":"//trunk/hello_world/cc:hello_test","run":1,"shard":1,"attempt":2,"configuration":{"id":"2136e65c67d9ba6b2652accfbf6533486c16692a484f4dc7ea7756de0edc13a6"}}},"children":[{"testResult":{"label":"//trunk/hello_world/cc:hello_test","run":1,"shard":1,"attempt":3,"configuration":{"id":"2136e65c67d9ba6b2652accfbf6533486c16692a484f4dc7ea7756de0edc13a6"}}}],"testResult":{"testActionOutput":[{"name":"test.log","uri":"file:///repos/trunk/trunk/hello_world/cc/hello_test/test_attempts/attempt_2.log"},{"name":"test.xml","uri":"file:///tmp/hello_test/test_attempts/attempt_2.xml"}],"testAttemptDurationMillis":"44","status":"FAILED","testAttemptStartMillisEpoch":"1734408655377","executionInfo":{"strategy":"linux-sandbox","timingBreakdown":{"child":[{"name":"parseTime","time":"0s"},{"name":"fetchTime","time":"0s"},{"name":"queueTime","time":"0s"},{"name":"uploadTime","time":"0s"},{"name":"setupTime","time":"0s"},{"name":"executionWallTime","time":"0.044s"},{"name":"processOutputsTime","time":"0s"},{"name":"networkTime","time":"0s"}],"name":"totalTime","time":"0.044s"}},"testAttemptStart":"2024-12-17T04:10:55.377Z","testAttemptDuration":"0.044s"}} +{"id":{"testResult":{"label":"//trunk/hello_world/cc:hello_test","run":1,"shard":1,"attempt":3,"configuration":{"id":"2136e65c67d9ba6b2652accfbf6533486c16692a484f4dc7ea7756de0edc13a6"}}},"testResult":{"testActionOutput":[{"name":"test.log","uri":"file:///repos/trunk/trunk/hello_world/cc/hello_test/test.log"},{"name":"test.xml","uri":"file:///tmp/hello_test/test.xml"}],"testAttemptDurationMillis":"41","status":"PASSED","testAttemptStartMillisEpoch":"1734408655425","executionInfo":{"strategy":"linux-sandbox","timingBreakdown":{"child":[{"name":"parseTime","time":"0s"},{"name":"fetchTime","time":"0s"},{"name":"queueTime","time":"0s"},{"name":"uploadTime","time":"0s"},{"name":"setupTime","time":"0s"},{"name":"executionWallTime","time":"0.041s"},{"name":"processOutputsTime","time":"0s"},{"name":"networkTime","time":"0s"}],"name":"totalTime","time":"0.041s"}},"testAttemptStart":"2024-12-17T04:10:55.425Z","testAttemptDuration":"0.041s"}} +{"id":{"testSummary":{"label":"//trunk/hello_world/cc:hello_test","configuration":{"id":"2136e65c67d9ba6b2652accfbf6533486c16692a484f4dc7ea7756de0edc13a6"}}},"testSummary":{"totalRunCount":3,"passed":[{"uri":"file:///repos/trunk/trunk/hello_world/cc/hello_test/test.log"}],"failed":[{"uri":"file:///repos/trunk/trunk/hello_world/cc/hello_test/test_attempts/attempt_1.log"},{"uri":"file:///repos/trunk/trunk/hello_world/cc/hello_test/test_attempts/attempt_2.log"}],"overallStatus":"FLAKY","firstStartTimeMillis":"1734408655425","lastStopTimeMillis":"1734408655466","totalRunDurationMillis":"41","runCount":1,"totalRunDuration":"0.041s","firstStartTime":"2024-12-17T04:10:55.425Z","lastStopTime":"2024-12-17T04:10:55.466Z","attemptCount":3}} +{"id":{"testResult":{"label":"//trunk/hello_world/cc_grpc:client_test","run":1,"shard":1,"attempt":1,"configuration":{"id":"2136e65c67d9ba6b2652accfbf6533486c16692a484f4dc7ea7756de0edc13a6"}}},"testResult":{"testActionOutput":[{"name":"test.log","uri":"file:///repos/trunk/trunk/hello_world/cc_grpc/client_test/test.log"},{"name":"test.xml","uri":"file:///tmp/client_test/test.xml"}],"testAttemptDurationMillis":"1061","status":"FAILED","testAttemptStartMillisEpoch":"1734408655322","executionInfo":{"strategy":"linux-sandbox","timingBreakdown":{"child":[{"name":"parseTime","time":"0s"},{"name":"fetchTime","time":"0s"},{"name":"queueTime","time":"0s"},{"name":"uploadTime","time":"0s"},{"name":"setupTime","time":"0s"},{"name":"executionWallTime","time":"1.061s"},{"name":"processOutputsTime","time":"0s"},{"name":"networkTime","time":"0s"}],"name":"totalTime","time":"1.061s"}},"testAttemptStart":"2024-12-17T04:10:55.322Z","testAttemptDuration":"1.061s"}} +{"id":{"testSummary":{"label":"//trunk/hello_world/cc_grpc:client_test","configuration":{"id":"2136e65c67d9ba6b2652accfbf6533486c16692a484f4dc7ea7756de0edc13a6"}}},"testSummary":{"totalRunCount":1,"failed":[{"uri":"file:///repos/trunk/trunk/hello_world/cc_grpc/client_test/test.log"}],"overallStatus":"FAILED","firstStartTimeMillis":"1734408655322","lastStopTimeMillis":"1734408656383","totalRunDurationMillis":"1061","runCount":1,"totalRunDuration":"1.061s","firstStartTime":"2024-12-17T04:10:55.322Z","lastStopTime":"2024-12-17T04:10:56.383Z","attemptCount":1}} +{"id":{"progress":{"opaqueCount":5}},"children":[{"progress":{"opaqueCount":6}}],"progress":{"stdout":"","stderr":""}} +{"id":{"progress":{"opaqueCount":6}},"children":[{"progress":{"opaqueCount":7}}],"progress":{"stdout":"","stderr":""}} +{"id":{"progress":{"opaqueCount":7}},"children":[{"progress":{"opaqueCount":8}}],"progress":{"stdout":"","stderr":""}} +{"id":{"progress":{"opaqueCount":8}},"children":[{"progress":{"opaqueCount":9}}],"progress":{"stdout":"","stderr":""}} +{"id":{"progress":{"opaqueCount":9}},"children":[{"progress":{"opaqueCount":10}}],"progress":{"stdout":"","stderr":""}} +{"id":{"progress":{"opaqueCount":10}},"children":[{"progress":{"opaqueCount":11}}],"progress":{"stdout":"","stderr":""}} +{"id":{"buildFinished":{}},"children":[{"buildToolLogs":{}}],"finished":{"overallSuccess":true,"finishTimeMillis":"1734408656395","exitCode":{"name":"SUCCESS"},"finishTime":"2024-12-17T04:10:56.395Z"}} +{"id":{"progress":{"opaqueCount":11}},"children":[{"progress":{"opaqueCount":12}},{"buildMetrics":{}}],"progress":{"stderr":""}} +{"id":{"buildMetrics":{}},"buildMetrics":{"actionSummary":{"actionsExecuted":"3","actionData":[{"mnemonic":"TestRunner","actionsExecuted":"2","firstStartedMs":"1734408655311","lastEndedMs":"1734408656393","systemTime":"0.132s","userTime":"1.023s"},{"mnemonic":"BazelWorkspaceStatusAction","actionsExecuted":"1","firstStartedMs":"1734408655309","lastEndedMs":"1734408655310"}],"runnerCount":[{"name":"total","count":3},{"name":"linux-sandbox","count":4,"execKind":"Local"}],"actionCacheStatistics":{"sizeInBytes":"21710463","hits":20,"misses":3,"missDetails":[{},{"reason":"DIFFERENT_DEPS"},{"reason":"DIFFERENT_ENVIRONMENT"},{"reason":"DIFFERENT_FILES"},{"reason":"CORRUPTED_CACHE_ENTRY"},{"reason":"NOT_CACHED"},{"reason":"UNCONDITIONAL_EXECUTION","count":3}]}},"memoryMetrics":{},"targetMetrics":{},"packageMetrics":{},"timingMetrics":{"cpuTimeInMs":"2040","wallTimeInMs":"1401","analysisPhaseTimeInMs":"9","executionPhaseTimeInMs":"1084"},"cumulativeMetrics":{"numAnalyses":7,"numBuilds":7},"artifactMetrics":{"sourceArtifactsRead":{},"outputArtifactsSeen":{"sizeInBytes":"716839","count":26},"outputArtifactsFromActionCache":{"sizeInBytes":"714374","count":20},"topLevelArtifacts":{}},"buildGraphMetrics":{"postInvocationSkyframeNodeCount":266751},"workerPoolMetrics":{}}} +{"id":{"buildToolLogs":{}},"buildToolLogs":{"log":[{"name":"elapsed time","contents":""},{"name":"critical path","contents":""},{"name":"process stats","contents":""},{"name":"command.profile.gz","uri":"file:///tmp/.cache/bazel/deadbeef/command.profile.gz"}]}} +{"id":{"progress":{"opaqueCount":12}},"progress":{},"lastMessage":true}