blob: 5fe93d10f3bec754af6a9b759a1922135b71b060 [file] [log] [blame]
// Copyright 2025 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 serde::Serialize;
use std::collections::HashMap;
use std::os::unix::process::ExitStatusExt;
use std::path::PathBuf;
use std::process::ExitStatus;
use tokio::process::Command;
/// A log of command invocations used to record details regarding the execution of the
/// test and post-processors. This is potentially useful for debugging issues with the
/// test framework itself.
#[derive(Serialize, Debug, Default)]
pub struct CommandInvocationLog {
/// The invocation of the test itself.
pub test: CommandInvocation,
/// The invocations of postprocessors by name.
#[serde(skip_serializing_if = "HashMap::is_empty")]
pub postprocessors: HashMap<String, CommandInvocation>,
}
impl CommandInvocationLog {
pub fn new() -> Self {
Self::default()
}
pub fn record_test(
&mut self,
command: &Command,
exit_status: ExitStatus,
stdout: Option<PathBuf>,
stderr: Option<PathBuf>,
) {
self.test = CommandInvocation::new(command, exit_status, stdout, stderr);
}
pub fn record_postprocessor(
&mut self,
command: &Command,
exit_status: ExitStatus,
stdout: Option<PathBuf>,
stderr: Option<PathBuf>,
) {
self.postprocessors.insert(
String::from(command.as_std().get_program().to_str().expect("program has UTF-8 name")),
CommandInvocation::new(command, exit_status, stdout, stderr),
);
}
}
/// Describes a single command invocation.
#[derive(Serialize, Debug, Default)]
pub struct CommandInvocation {
/// The path of the invoked program.
pub program_path: PathBuf,
/// The arguments passed to the program.
#[serde(skip_serializing_if = "Vec::is_empty")]
pub args: Vec<String>,
/// The environment provided to the program.
#[serde(skip_serializing_if = "HashMap::is_empty")]
pub envs: HashMap<String, Option<String>>,
/// The directory in which the command was invoked.
pub current_dir: PathBuf,
/// The exit status of the command invocation.
pub exit_status: i32,
/// The stdout artifact, if anything was written to stdout.
#[serde(skip_serializing_if = "Option::is_none")]
pub stdout_artifact: Option<PathBuf>,
/// The stderr artifact, if anything was written to stderr.
#[serde(skip_serializing_if = "Option::is_none")]
pub stderr_artifact: Option<PathBuf>,
}
impl CommandInvocation {
pub fn new(
command: &Command,
exit_status: ExitStatus,
stdout: Option<PathBuf>,
stderr: Option<PathBuf>,
) -> Self {
let cmd = command.as_std();
Self {
program_path: PathBuf::from(cmd.get_program()),
args: cmd
.get_args()
.map(|arg| String::from(arg.to_str().expect("args are UTF-8")))
.collect(),
envs: cmd
.get_envs()
.map(|(k, v)| {
(
String::from(k.to_str().expect("env variables have UTF-8 names")),
v.map(|val| {
String::from(val.to_str().expect("env variables have UTF-8 values"))
}),
)
})
.collect(),
current_dir: cmd.get_current_dir().expect("command has current dir").to_path_buf(),
exit_status: exit_status.into_raw(),
stdout_artifact: stdout,
stderr_artifact: stderr,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[fuchsia::test]
fn test_execution_log() {
let mut under_test = CommandInvocationLog::new();
let mut command = Command::new("/etc/test_binary");
command.arg("test_arg1").arg("test_arg2");
command.env("test_env1", "test_env1_value").env("test_env2", "test_env2_value");
command.current_dir("/tmp");
under_test.record_test(
&command,
ExitStatus::from_raw(123),
Some(PathBuf::from("test_stdout")),
Some(PathBuf::from("test_stderr")),
);
let mut command = Command::new("/etc/postprocessor1_binary");
command.arg("pp1_arg1");
command.env("pp1_env1", "pp1_env1_value");
command.current_dir("/etc");
under_test.record_postprocessor(&command, ExitStatus::from_raw(124), None, None);
let value = serde_json::to_value(&under_test)
.expect("invocation log conversion to value should succeed");
assert_eq!(
value,
json!({
"test": {
"program_path": "/etc/test_binary",
"args": ["test_arg1", "test_arg2"],
"envs": {
"test_env1": "test_env1_value",
"test_env2": "test_env2_value"
},
"current_dir": "/tmp",
"exit_status": 123,
"stdout_artifact": "test_stdout",
"stderr_artifact": "test_stderr"
},
"postprocessors": {
"/etc/postprocessor1_binary": {
"program_path": "/etc/postprocessor1_binary",
"args": ["pp1_arg1"],
"envs": {
"pp1_env1": "pp1_env1_value"
},
"current_dir": "/etc",
"exit_status": 124,
}
}
})
);
}
}