use std::collections::BTreeSet;
use std::path::PathBuf;
use runner_manager_platform::paths::AppPaths;
use runner_manager_platform::runner_root::default_runner_root;
use runner_manager_platform::secrets::{PlatformSecretStore, SecretScope};
use runner_manager_platform::service::ServiceIdentity;
use crate::cli_chains::action::ActionKind;
use super::run::CaseRun;
#[derive(Debug, Clone)]
pub struct StandardFootprint {
pub locations: Vec<(String, PathBuf, bool)>,
}
impl StandardFootprint {
#[must_use]
pub fn snapshot() -> Self {
let mut locations: Vec<(String, PathBuf)> = Vec::new();
if let Ok(paths) = AppPaths::discover() {
for (name, path) in paths.all() {
locations.push((format!("standard {name} directory"), path.to_path_buf()));
}
locations.push((
"standard database".to_string(),
paths.config_dir().join("runner-manager.sqlite3"),
));
locations.push((
"standard service install record".to_string(),
paths.config_dir().join("service.toml"),
));
if let Ok(root) = default_runner_root(&paths) {
locations.push((
"platform-default runner root".to_string(),
root.as_path().to_path_buf(),
));
}
}
for scope in [SecretScope::Machine, SecretScope::User] {
if let Ok(store) = PlatformSecretStore::standard(scope) {
locations.push((
format!("standard {scope}-scoped secret store"),
store.guard(),
));
}
}
Self {
locations: locations
.into_iter()
.map(|(what, path)| {
let existed = path.exists();
(what, path, existed)
})
.collect(),
}
}
#[must_use]
pub fn appeared_since(&self) -> Vec<String> {
self.locations
.iter()
.filter(|(_, path, existed)| !existed && path.exists())
.map(|(what, path, _)| format!("{what} {} appeared", path.display()))
.collect()
}
}
#[must_use]
pub fn invocation_problems(run: &CaseRun<'_>) -> Vec<String> {
let mut problems = Vec::new();
let data = run.resolver.data.to_string_lossy().into_owned();
let allowed: BTreeSet<[&str; 2]> = ActionKind::ALL
.iter()
.map(|kind| kind.command_path())
.collect();
let announcement = format!("talking to {} instead of GitHub", run.github_base);
for invocation in run.invocations() {
let argv = &invocation.argv;
if argv.len() < 4 || argv[0] != "--data-dir" || argv[1] != data {
problems.push(format!(
"{argv:?} does not start with this scenario's --data-dir {data}"
));
continue;
}
let leaf = [argv[2].as_str(), argv[3].as_str()];
if !allowed.contains(&leaf) {
problems.push(format!("{argv:?} runs a command outside the allowlist"));
}
if !invocation.stderr.contains(&announcement) {
problems.push(format!(
"{argv:?} did not report talking to the case's loopback fixture {}",
run.github_base
));
}
}
let identity = ServiceIdentity::fixture(&run.service_tag);
if !identity.is_fixture() || identity.name() == ServiceIdentity::product().name() {
problems.push(format!(
"the service tag {} selects {}, which is not a disposable fixture",
run.service_tag,
identity.name()
));
}
if !run.service_tag.contains(&run.case.id.to_string()) {
problems.push(format!(
"the service tag {} does not name its case",
run.service_tag
));
}
problems
}