use std::collections::BTreeMap;
use std::path::PathBuf;
use std::rc::Rc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Mutex;
use harn_vm::VmValue;
use once_cell::sync::Lazy;
use crate::error::HostlibError;
use crate::tools::payload::{optional_bool, require_dict_arg, require_string};
use crate::tools::response::ResponseBuilder;
use crate::tools::test_parsers;
pub(crate) const NAME: &str = "hostlib_tools_inspect_test_results";
#[derive(Debug, Clone)]
pub(crate) struct RawArtifacts {
pub(crate) stdout: String,
pub(crate) stderr: String,
#[allow(dead_code)]
pub(crate) exit_code: i32,
pub(crate) junit_path: Option<PathBuf>,
pub(crate) ecosystem: Option<String>,
#[allow(dead_code)]
pub(crate) argv: Vec<String>,
}
#[derive(Debug, Clone, Copy, Default)]
pub(crate) struct TestSummaryData {
pub(crate) passed: u32,
pub(crate) failed: u32,
pub(crate) skipped: u32,
}
impl RawArtifacts {
pub(crate) fn compute_summary(&self) -> Option<TestSummaryData> {
let records = self.parse_records();
if records.is_empty() {
return None;
}
let mut data = TestSummaryData::default();
for r in &records {
match r.status {
test_parsers::Status::Passed => data.passed += 1,
test_parsers::Status::Failed | test_parsers::Status::Errored => data.failed += 1,
test_parsers::Status::Skipped => data.skipped += 1,
}
}
Some(data)
}
fn parse_records(&self) -> Vec<test_parsers::TestRecord> {
if let Some(path) = self.junit_path.as_ref() {
if let Ok(bytes) = std::fs::read(path) {
if let Ok(records) = test_parsers::parse_junit_xml(&bytes) {
return records;
}
}
}
if let Some(eco) = self.ecosystem.as_deref() {
if eco == "cargo" {
return test_parsers::parse_cargo_libtest(&self.stdout);
}
if eco == "go" {
return test_parsers::parse_go_text(&self.stdout, &self.stderr);
}
}
if self.stdout.contains("test result:") {
return test_parsers::parse_cargo_libtest(&self.stdout);
}
Vec::new()
}
}
#[derive(Default)]
struct HandleStore {
entries: BTreeMap<String, RawArtifacts>,
}
static STORE: Lazy<Mutex<HandleStore>> = Lazy::new(|| Mutex::new(HandleStore::default()));
static HANDLE_COUNTER: AtomicU64 = AtomicU64::new(1);
pub(crate) fn store_run(artifacts: RawArtifacts) -> String {
let id = HANDLE_COUNTER.fetch_add(1, Ordering::SeqCst);
let handle = format!("htr-{:x}-{id}", std::process::id());
let mut store = STORE.lock().expect("hostlib test handle store poisoned");
store.entries.insert(handle.clone(), artifacts);
handle
}
pub(crate) fn handle(args: &[VmValue]) -> Result<VmValue, HostlibError> {
let map = require_dict_arg(NAME, args)?;
let handle = require_string(NAME, &map, "result_handle")?;
let include_passing = optional_bool(NAME, &map, "include_passing")?.unwrap_or(false);
let artifacts = {
let store = STORE.lock().expect("hostlib test handle store poisoned");
store
.entries
.get(&handle)
.cloned()
.ok_or(HostlibError::InvalidParameter {
builtin: NAME,
param: "result_handle",
message: format!("no test results stored under handle {handle}"),
})?
};
let mut records = artifacts.parse_records();
if !include_passing {
records.retain(|r| !matches!(r.status, test_parsers::Status::Passed));
}
let entries: Vec<VmValue> = records
.into_iter()
.map(|r| VmValue::Dict(Rc::new(record_to_map(r))))
.collect();
Ok(ResponseBuilder::new()
.str("result_handle", handle)
.list("tests", entries)
.build())
}
fn record_to_map(record: test_parsers::TestRecord) -> BTreeMap<String, VmValue> {
let mut map = BTreeMap::new();
map.insert("name".to_string(), VmValue::String(Rc::from(record.name)));
map.insert(
"status".to_string(),
VmValue::String(Rc::from(record.status.as_str())),
);
map.insert(
"duration_ms".to_string(),
VmValue::Int(record.duration_ms as i64),
);
map.insert(
"message".to_string(),
record
.message
.map(|m| VmValue::String(Rc::from(m)))
.unwrap_or(VmValue::Nil),
);
map.insert(
"stdout".to_string(),
record
.stdout
.map(|s| VmValue::String(Rc::from(s)))
.unwrap_or(VmValue::Nil),
);
map.insert(
"stderr".to_string(),
record
.stderr
.map(|s| VmValue::String(Rc::from(s)))
.unwrap_or(VmValue::Nil),
);
map.insert(
"path".to_string(),
record
.path
.map(|p| VmValue::String(Rc::from(p)))
.unwrap_or(VmValue::Nil),
);
map.insert(
"line".to_string(),
record.line.map(VmValue::Int).unwrap_or(VmValue::Nil),
);
map
}