use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::{Command, Output, Stdio};
const FIXTURES_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures");
fn fixture_path(name: &str) -> PathBuf {
Path::new(FIXTURES_DIR).join(name)
}
fn sbom_tools_bin() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_sbom-tools"))
}
fn base_command() -> Command {
let mut cmd = Command::new(sbom_tools_bin());
cmd.arg("--no-color");
cmd.env("RUST_LOG", "error");
cmd.env("RUST_LOG_STYLE", "never");
cmd
}
fn stderr(output: &Output) -> String {
String::from_utf8_lossy(&output.stderr).into_owned()
}
fn run_with_stdin(mut cmd: Command, input: &[u8]) -> Output {
cmd.stdin(Stdio::piped());
cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::piped());
let mut child = cmd.spawn().expect("binary should launch");
child
.stdin
.take()
.expect("child stdin should be piped")
.write_all(input)
.expect("should write fixture to stdin");
child.wait_with_output().expect("child should complete")
}
fn large_fixture() -> Option<PathBuf> {
let dir = Path::new(FIXTURES_DIR).join("cyclonedx");
std::fs::read_dir(dir)
.ok()?
.filter_map(Result::ok)
.find_map(|e| {
let p = e.path();
let name = p.file_name()?.to_string_lossy().to_string();
(name.starts_with("sbom-cyclone-dx-json-") && name.ends_with(".json")).then_some(p)
})
}
#[test]
fn quality_reads_sbom_from_stdin() {
let sbom = std::fs::read(fixture_path("cyclonedx/minimal.cdx.json")).expect("fixture readable");
let mut cmd = base_command();
cmd.arg("quality")
.arg("-")
.args(["--profile", "standard", "-o", "json"]);
let output = run_with_stdin(cmd, &sbom);
assert!(output.status.success(), "stderr: {}", stderr(&output));
let text = String::from_utf8_lossy(&output.stdout);
assert!(
text.contains("overall_score") || text.contains("\"report\""),
"expected a quality JSON report, got: {text}"
);
}
#[test]
fn validate_reads_sbom_from_stdin() {
let sbom = std::fs::read(fixture_path("cyclonedx/minimal.cdx.json")).expect("fixture readable");
let mut cmd = base_command();
cmd.arg("validate")
.arg("-")
.args(["--standard", "ntia", "-o", "json"]);
let output = run_with_stdin(cmd, &sbom);
let combined = format!(
"{}{}",
String::from_utf8_lossy(&output.stdout),
stderr(&output)
);
assert!(
!combined.contains("No such file") && !combined.contains("Failed to read"),
"validate should have read from stdin, got: {combined}"
);
assert!(
combined.contains("compliant")
|| combined.contains("Compliance")
|| combined.contains("NTIA"),
"expected NTIA compliance output, got: {combined}"
);
}
#[test]
fn diff_accepts_one_stdin_side() {
let new_sbom = std::fs::read(fixture_path("demo-new.cdx.json")).expect("fixture readable");
let mut cmd = base_command();
cmd.arg("diff")
.arg(fixture_path("demo-old.cdx.json"))
.arg("-")
.args(["-o", "summary"]);
let output = run_with_stdin(cmd, &new_sbom);
let combined = format!(
"{}{}",
String::from_utf8_lossy(&output.stdout),
stderr(&output)
);
assert!(
!combined.contains("Failed to read") && !combined.contains("No such file"),
"diff should read the new side from stdin, got: {combined}"
);
}
#[test]
fn diff_rejects_two_stdin_sides() {
let mut cmd = base_command();
cmd.arg("diff").arg("-").arg("-").args(["-o", "summary"]);
let output = run_with_stdin(cmd, b"{}");
assert!(!output.status.success(), "two stdins must be rejected");
assert!(
stderr(&output).contains("both SBOMs from stdin")
|| stderr(&output).contains("only one '-'"),
"expected a two-stdin rejection message, got: {}",
stderr(&output)
);
}
#[test]
#[ignore = "large fixture; run explicitly"]
fn parse_and_diff_large_fixture() {
let Some(path) = large_fixture() else {
eprintln!("large fixture not present; skipping");
return;
};
let sbom = sbom_tools::parsers::parse_sbom(&path).expect("large fixture should parse");
assert!(
sbom.component_count() > 100,
"expected a large component set, got {}",
sbom.component_count()
);
let config = sbom_tools::config::DiffConfigBuilder::new()
.old_path(path.clone())
.new_path(path.clone())
.build()
.expect("diff config should build");
let other = sbom_tools::parsers::parse_sbom(&path).expect("second parse should succeed");
let result = sbom_tools::pipeline::compute_diff(&config, &sbom, &other)
.expect("self-diff should succeed");
assert_eq!(
result.summary.total_changes, 0,
"self-diff should report no changes"
);
}