use crate::core::types::{Machine, Resource};
use crate::transport;
pub(crate) fn missing_outputs(resource: &Resource, machine: &Machine) -> Vec<String> {
if resource.output_artifacts.is_empty() || !crate::transport::machine_is_local(machine) {
return Vec::new();
}
let base = crate::core::task::probe::probe_base_dir(resource);
resource
.output_artifacts
.iter()
.filter(|a| !crate::core::task::probe::resolve_under(&base, a).exists())
.cloned()
.collect()
}
pub(crate) fn unproduced_outputs_error(resource: &Resource, machine: &Machine) -> Option<String> {
let missing = missing_outputs(resource, machine);
if missing.is_empty() {
None
} else {
Some(missing_outputs_error(&missing))
}
}
pub(crate) fn missing_outputs_error(missing: &[String]) -> String {
format!(
"command exited 0 but declared output artifact(s) were not produced: {}. \
The resource is NOT converged — a script can exit 0 without doing its \
job (a swallowed error, a wrong path, or a command that consumed the \
rest of the script from stdin).",
missing.join(", ")
)
}
pub(crate) fn run_pre_apply_hook(
machine: &Machine,
hook: &str,
timeout: Option<u64>,
) -> Option<String> {
match transport::exec_script_timeout(machine, hook, timeout) {
Ok(out) if !out.success() => Some(format!(
"pre_apply hook failed (exit {}): {}",
out.exit_code,
out.stderr.trim()
)),
Err(e) => Some(format!("pre_apply hook error: {e}")),
_ => None,
}
}
pub(crate) fn check_post_hook(
machine: &Machine,
hook: &str,
timeout: Option<u64>,
) -> Option<String> {
match transport::exec_script_timeout(machine, hook, timeout) {
Ok(pout) if !pout.success() => Some(format!(
"post_apply hook failed (exit {}): {}",
pout.exit_code,
pout.stderr.trim()
)),
Err(e) => Some(format!("post_apply hook error: {e}")),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::types::ResourceType;
fn local() -> Machine {
serde_yaml_ng::from_str("hostname: localhost\naddr: localhost\n").unwrap()
}
fn remote() -> Machine {
serde_yaml_ng::from_str("hostname: far\naddr: 10.9.9.9\n").unwrap()
}
fn task(dir: &std::path::Path, outs: &[&str]) -> Resource {
Resource {
resource_type: ResourceType::Task,
output_artifacts: outs.iter().map(|s| s.to_string()).collect(),
working_dir: Some(dir.display().to_string()),
..Default::default()
}
}
#[test]
fn a_missing_declared_artifact_is_reported() {
let d = tempfile::tempdir().unwrap();
let r = task(d.path(), &["second.txt"]);
assert_eq!(
missing_outputs(&r, &local()),
vec!["second.txt".to_string()]
);
}
#[test]
fn a_produced_artifact_is_not_reported() {
let d = tempfile::tempdir().unwrap();
std::fs::write(d.path().join("second.txt"), "ok").unwrap();
assert!(missing_outputs(&task(d.path(), &["second.txt"]), &local()).is_empty());
}
#[test]
fn only_the_missing_ones_are_named() {
let d = tempfile::tempdir().unwrap();
std::fs::write(d.path().join("there.txt"), "ok").unwrap();
assert_eq!(
missing_outputs(&task(d.path(), &["there.txt", "gone.txt"]), &local()),
vec!["gone.txt".to_string()]
);
}
#[test]
fn a_directory_artifact_counts_as_produced() {
let d = tempfile::tempdir().unwrap();
std::fs::create_dir_all(d.path().join("build")).unwrap();
assert!(missing_outputs(&task(d.path(), &["build"]), &local()).is_empty());
}
#[test]
fn a_resource_declaring_no_outputs_is_not_verified() {
let d = tempfile::tempdir().unwrap();
assert!(missing_outputs(&task(d.path(), &[]), &local()).is_empty());
}
#[test]
fn a_remote_resource_is_never_verified_against_this_host() {
let d = tempfile::tempdir().unwrap();
assert!(
missing_outputs(&task(d.path(), &["second.txt"]), &remote()).is_empty(),
"a remote target must not be judged by this host's filesystem"
);
}
#[test]
fn the_apply_entry_point_is_silent_when_there_is_nothing_to_answer_for() {
let d = tempfile::tempdir().unwrap();
std::fs::write(d.path().join("x"), "ok").unwrap();
assert!(unproduced_outputs_error(&task(d.path(), &["x"]), &local()).is_none());
assert!(unproduced_outputs_error(&task(d.path(), &[]), &local()).is_none());
assert!(unproduced_outputs_error(&task(d.path(), &["gone"]), &remote()).is_none());
assert!(unproduced_outputs_error(&task(d.path(), &["gone"]), &local()).is_some());
}
#[test]
fn the_error_names_the_artifacts_and_the_likely_cause() {
let e = missing_outputs_error(&["a.txt".into(), "b.txt".into()]);
assert!(e.contains("a.txt") && e.contains("b.txt"), "{e}");
assert!(e.contains("NOT converged"), "{e}");
}
}