use super::{CheckFinding, finding};
use crate::compare::compare;
use crate::os_support::platform_guard;
use crate::pipeline::PipelineNode;
pub(super) const PLATFORMS: &[&str] = &["macos", "linux", "windows", "unknown"];
pub(super) fn narrow(platforms: &[&'static str], node: &PipelineNode) -> Vec<&'static str> {
let Some((op, expected)) = node.when.as_ref().and_then(platform_guard) else {
return platforms.to_vec();
};
let allowed = |p: &&str| compare(p, op, expected).is_ok();
platforms.iter().copied().filter(allowed).collect()
}
pub(super) fn guard_finding(node: &PipelineNode) -> Option<CheckFinding> {
let (_, expected) = node.when.as_ref().and_then(platform_guard)?;
(!PLATFORMS.contains(&expected)).then(|| {
let message = format!(
"compares {{{{ctx.platform}}}} with \"{expected}\", which it never reads; \
it reads macos, linux, windows or unknown"
);
finding(node, Some("when"), message)
})
}
pub(super) fn unreachable_finding(node: &PipelineNode, platforms: &[&str]) -> Option<CheckFinding> {
let message = "never runs: the platform guards on its path rule out every OS";
platforms
.is_empty()
.then(|| finding(node, None, message.to_string()))
}
pub(super) fn io_requires_finding(node: &PipelineNode) -> Option<CheckFinding> {
let message = "\"requires\" has no effect on an input or output node; \
put it on the step or group that needs the tool";
(!node.requires.is_empty()).then(|| finding(node, Some("requires"), message.to_string()))
}
#[cfg(test)]
mod tests {
use serde_json::{Value, json};
use crate::check::check_pipeline;
use crate::context::ProcessContext;
use crate::errors::BntoError;
use crate::processor::{NodeInput, NodeOutput, NodeProcessor};
use crate::progress::ProgressReporter;
use crate::registry::NodeRegistry;
struct WindowsOnly;
impl NodeProcessor for WindowsOnly {
fn name(&self) -> &str {
"windows-only"
}
fn platform_problems(
&self,
_params: &serde_json::Map<String, Value>,
platforms: &[&str],
) -> Vec<String> {
let elsewhere: Vec<&str> = platforms
.iter()
.copied()
.filter(|p| *p != "windows")
.collect();
if elsewhere.is_empty() {
return Vec::new();
}
vec![format!(
"needs Windows, but can run on {}",
elsewhere.join(", ")
)]
}
fn process(
&self,
_input: NodeInput,
_progress: &ProgressReporter,
_ctx: &dyn ProcessContext,
) -> Result<NodeOutput, BntoError> {
unreachable!("check never runs a node")
}
}
fn registry() -> NodeRegistry {
let mut registry = NodeRegistry::new();
registry.register("windows-only", Box::new(WindowsOnly));
registry
}
fn platform(op: &str, expected: &str) -> Value {
json!({ "value": "{{ctx.platform}}", "op": op, "expected": expected })
}
fn step(id: &str) -> Value {
json!({ "id": id, "type": "windows-only" })
}
fn group(id: &str, when: Value, nodes: Vec<Value>) -> Value {
json!({ "id": id, "type": "group", "when": when, "nodes": nodes })
}
fn findings(nodes: Vec<Value>) -> Vec<String> {
let def = serde_json::from_value(json!({ "nodes": nodes })).unwrap();
check_pipeline(&def, ®istry())
.findings
.iter()
.map(|f| format!("{}: {}", f.node_id, f.message))
.collect()
}
#[test]
fn a_guard_on_a_value_ctx_platform_never_reads_is_a_finding() {
for expected in ["darwin", "macOS", "win32", "freebsd"] {
let found = findings(vec![group(
"g",
platform("equals", expected),
vec![step("s")],
)]);
assert!(
found
.iter()
.any(|f| f.starts_with("g: compares {{ctx.platform}} with")
&& f.contains(&format!("\"{expected}\""))),
"{expected}: {found:?}"
);
}
}
#[test]
fn every_value_ctx_platform_reads_is_fine_in_a_guard() {
for expected in ["macos", "linux", "unknown"] {
let nodes = vec![group(
"g",
platform("not-equals", expected),
vec![step("s")],
)];
let found = findings(nodes);
assert!(
!found.iter().any(|f| f.contains("compares")),
"{expected}: {found:?}"
);
}
}
#[test]
fn a_step_contradictory_guards_rule_out_of_every_os_never_runs() {
let inner = group("lin", platform("equals", "linux"), vec![step("s")]);
let found = findings(vec![group("mac", platform("equals", "macos"), vec![inner])]);
assert!(
found.contains(
&"s: never runs: the platform guards on its path rule out every OS".to_string()
),
"{found:?}"
);
}
#[test]
fn a_branch_for_other_oses_still_runs_somewhere() {
let found = findings(vec![group(
"other",
platform("equals", "unknown"),
vec![step("s")],
)]);
assert!(!found.iter().any(|f| f.contains("never runs")), "{found:?}");
}
#[test]
fn a_processor_hears_the_platforms_its_step_can_run_on() {
let found = findings(vec![step("anywhere")]);
assert!(
found.contains(
&"anywhere: needs Windows, but can run on macos, linux, unknown".to_string()
),
"{found:?}"
);
let found = findings(vec![group(
"win",
platform("equals", "windows"),
vec![step("guarded")],
)]);
assert!(!found.iter().any(|f| f.starts_with("guarded")), "{found:?}");
}
#[test]
fn requires_on_an_io_node_is_a_finding() {
let input = json!({ "id": "input", "type": "input", "requires": [{ "binary": "jq" }] });
let found = findings(vec![
input,
json!({ "id": "s", "type": "windows-only",
"when": platform("equals", "windows") }),
]);
assert!(
found
.iter()
.any(|f| f.starts_with("input: \"requires\" has no effect")),
"{found:?}"
);
}
}