blob: 228346bd0ed219d789f4ff72697412be43b2ec5d [file]
// Copyright 2026 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use std::io::{self, BufRead, BufReader, Read, Write};
use std::path::PathBuf;
use std::process::{Command, Stdio};
use std::{env, fs};
use anyhow::{Context, Result};
use argh::FromArgs;
use gtest_test_parser::{ChildGuard, TestResult, parse_str};
const TEST_OUTPUT_SUMMARY_PATH_ENV_KEY: &str = "TEST_OUTPUT_SUMMARY_PATH";
#[derive(FromArgs, Debug)]
/// Reads stdout from the test command (or from --file/stdin if no command given),
/// and writes a JSON formatted summary of parsed GTest cases.
struct Args {
/// path to a log file to parse instead of running a subprocess command.
#[argh(option)]
file: Option<PathBuf>,
/// path to write the output JSON summary to. Overrides TEST_OUTPUT_SUMMARY_PATH.
#[argh(option)]
output_summary: Option<PathBuf>,
/// test command and arguments to execute.
#[argh(positional, greedy)]
command: Vec<String>,
}
fn main() -> Result<()> {
let args: Args = argh::from_env();
if args.file.is_some() && !args.command.is_empty() {
anyhow::bail!("Cannot specify both --file and a command to run");
}
let output_summary_path = args
.output_summary
.or_else(|| env::var_os(TEST_OUTPUT_SUMMARY_PATH_ENV_KEY).map(PathBuf::from));
let mut exit_code = 0;
let input_text: String = if let Some(file_path) = args.file {
let bytes = fs::read(&file_path)
.with_context(|| format!("error reading input file {}", file_path.display()))?;
String::from_utf8_lossy(&bytes).into_owned()
} else if args.command.is_empty() {
let mut buffer = Vec::new();
io::stdin().read_to_end(&mut buffer).context("error reading stdin")?;
String::from_utf8_lossy(&buffer).into_owned()
} else {
let binary = &args.command[0];
let command_arguments = &args.command[1..];
let mut child = Command::new(binary)
.args(command_arguments)
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.spawn()
.with_context(|| format!("error running test binary {binary}"))?;
let child_stdout = child.stdout.take().expect("child stdout was piped");
let guard = ChildGuard::new(child);
let mut reader = BufReader::new(child_stdout);
let mut captured_lines = Vec::new();
let stdout = io::stdout();
let mut out = stdout.lock();
let mut line_buf = Vec::new();
while reader.read_until(b'\n', &mut line_buf)? > 0 {
out.write_all(&line_buf)?;
out.flush()?;
let line_str = String::from_utf8_lossy(&line_buf);
captured_lines.push(line_str.trim_end_matches(&['\r', '\n'][..]).to_string());
line_buf.clear();
}
let status = guard.wait().context("error waiting for test binary process")?;
if !status.success() {
exit_code = status.code().unwrap_or(1);
}
captured_lines.join("\n")
};
let cases = parse_str(&input_text);
let result = TestResult { cases };
if let Some(summary_path) = output_summary_path {
if let Some(parent_dir) = summary_path.parent() {
if !parent_dir.as_os_str().is_empty() {
fs::create_dir_all(parent_dir).with_context(|| {
format!("error creating directory {}", parent_dir.display())
})?;
}
}
let file = fs::File::create(&summary_path)
.with_context(|| format!("error creating summary file {}", summary_path.display()))?;
let mut writer = io::BufWriter::new(file);
serde_json::to_writer_pretty(&mut writer, &result)
.context("error writing test summary JSON to file")?;
writer.flush().context("error flushing test summary file")?;
} else if args.command.is_empty() {
// In --file or stdin mode, logs were not mirrored to stdout, so outputting
// JSON summary to stdout is clean and expected. If a command was executed
// without an output summary path, we intentionally skip emitting JSON to
// avoid mixing test execution logs with JSON.
let stdout = io::stdout();
let mut handle = stdout.lock();
serde_json::to_writer_pretty(&mut handle, &result)
.context("error writing test summary JSON to stdout")?;
writeln!(handle)?;
}
if exit_code != 0 {
std::process::exit(exit_code);
}
Ok(())
}