#![expect(
clippy::significant_drop_tightening,
reason = "fixture serialization and directory ownership must last through output/filesystem assertions"
)]
use super::*;
use crate::{
test_support::{Fixture, LIMITS},
tool::{ExecutionFailure, OutputStream},
};
use std::os::unix::ffi::OsStringExt as _;
use std::{ffi::OsString, fs};
#[test]
fn normalization_preserves_the_canic_line_contract_including_blank_lines() {
for (input, expected) in [
("", ""),
("service : {}", "service : {}\n"),
(
"// \r\nservice : { \r\n query : () -> ();\t\r\n}",
"//\nservice : {\n query : () -> ();\n}\n",
),
("service : {}\n\n", "service : {}\n\n"),
(" indented\u{2003}\n\t\n", " indented\n\n"),
("standalone\rcarriage", "standalone\rcarriage\n"),
] {
assert_eq!(normalize(input.as_bytes(), 1024).unwrap(), expected);
}
}
#[test]
fn normalization_bounds_input_and_added_newlines_without_lossy_utf8() {
assert!(matches!(
normalize(&[0xff], 10),
Err(NormalizationError::Utf8(_))
));
assert!(matches!(
normalize(b"abc", 2),
Err(NormalizationError::InputLimit {
actual: 3,
limit: 2
})
));
assert!(matches!(
normalize(b"abc", 3),
Err(NormalizationError::OutputLimit { limit: 3 })
));
assert_eq!(normalize(b"abc", 4).unwrap(), "abc\n");
assert_eq!(normalize(b"abc\n", 4).unwrap(), "abc\n");
assert_eq!(normalize(b"", 0).unwrap(), "");
}
#[test]
fn extraction_retains_source_tool_and_original_output_evidence() {
let fixture = Fixture::new();
let source = fixture.source();
let original = fs::read(&source).unwrap();
let tool = fixture.tool();
let candid = extract(&tool, &source, &fixture.context(&[]), 8, LIMITS).unwrap();
assert_eq!(
candid.text,
"//\nservice : {\n query : () -> () query;\n}\n"
);
assert_eq!(candid.source_identity, hash_file(&source, 8).unwrap());
assert_eq!(candid.tool_identity, tool.identity());
assert!(candid.evidence.stdout.contains(&b'\r'));
assert!(
candid
.evidence
.status
.is_some_and(|status| status.success())
);
assert_eq!(fs::read(source).unwrap(), original);
}
#[test]
fn extraction_rejects_missing_relative_and_oversized_sources_before_invocation() {
let fixture = Fixture::new();
let source = fixture.source();
let tool = fixture.tool();
let marker = fixture.root.join("invocations");
let environment = [(
OsString::from("FIXTURE_MARKER"),
marker.as_os_str().to_owned(),
)];
let context = fixture.context(&environment);
assert!(matches!(
extract(&tool, Path::new("source.wasm"), &context, 8, LIMITS),
Err(ExtractionError::SourcePath)
));
assert!(matches!(
extract(&tool, &source, &context, 7, LIMITS),
Err(ExtractionError::Input(ArtifactError::LimitExceeded {
limit: 7
}))
));
assert!(matches!(
extract(&tool, &fixture.root.join("missing"), &context, 8, LIMITS),
Err(ExtractionError::Input(ArtifactError::Io(_)))
));
assert!(!marker.exists());
}
#[test]
fn extractor_failure_invalid_utf8_and_output_overflow_keep_bounded_evidence() {
let fixture = Fixture::new();
let source = fixture.source();
let tool = fixture.tool();
let environment = [(
OsString::from("FIXTURE_CANDID_MODE"),
OsString::from("fail"),
)];
let error = extract(&tool, &source, &fixture.context(&environment), 8, LIMITS);
assert!(
matches!(error, Err(ExtractionError::Tool(ToolError::Execution(error))) if matches!(error.failure, ExecutionFailure::ExitStatus) && error.evidence.stderr == b"extract failed")
);
let environment = [(
OsString::from("FIXTURE_CANDID_MODE"),
OsString::from("invalid-utf8"),
)];
let error = extract(&tool, &source, &fixture.context(&environment), 8, LIMITS);
assert!(
matches!(error, Err(ExtractionError::Normalize { source: NormalizationError::Utf8(_), evidence }) if evidence.stdout == [0xff])
);
let error = extract(
&tool,
&source,
&fixture.context(&[]),
8,
OutputLimits {
stdout_bytes: 10,
..LIMITS
},
);
assert!(
matches!(error, Err(ExtractionError::Tool(ToolError::Execution(error))) if matches!(error.failure, ExecutionFailure::OutputLimit { stream: OutputStream::Stdout }) && error.evidence.stdout.len() == 10)
);
assert_eq!(fs::read(source).unwrap(), b"\0asm\x01\0\0\0");
}
#[test]
fn normalized_output_overflow_retains_successful_raw_extractor_bytes() {
let fixture = Fixture::new();
let source = fixture.source();
let tool = fixture.tool();
let environment = [(
OsString::from("FIXTURE_CANDID_MODE"),
OsString::from("unterminated"),
)];
let error = extract(
&tool,
&source,
&fixture.context(&environment),
8,
OutputLimits {
stdout_bytes: b"service : {}".len(),
..LIMITS
},
);
assert!(
matches!(error, Err(ExtractionError::Normalize { source: NormalizationError::OutputLimit { .. }, evidence }) if evidence.stdout == b"service : {}" && evidence.status.is_some_and(|status| status.success()))
);
}
#[test]
fn source_changes_or_failed_reinspection_reject_successful_output() {
let fixture = Fixture::new();
let tool = fixture.tool();
let source = fixture.source();
let environment = [(
OsString::from("FIXTURE_CANDID_MODE"),
OsString::from("mutate"),
)];
let error = extract(&tool, &source, &fixture.context(&environment), 100, LIMITS);
assert!(
matches!(error, Err(ExtractionError::SourceChanged { before, after, evidence }) if before != after && evidence.stdout == b"service : {}\n")
);
let source = fixture.source();
let environment = [(
OsString::from("FIXTURE_CANDID_MODE"),
OsString::from("grow"),
)];
let error = extract(&tool, &source, &fixture.context(&environment), 8, LIMITS);
assert!(
matches!(error, Err(ExtractionError::SourceInspection { source: ArtifactError::LimitExceeded { limit: 8 }, evidence }) if evidence.stdout == b"service : {}\n")
);
let source = fixture.source();
let environment = [(
OsString::from("FIXTURE_CANDID_MODE"),
OsString::from("remove"),
)];
let error = extract(&tool, &source, &fixture.context(&environment), 8, LIMITS);
assert!(
matches!(error, Err(ExtractionError::SourceInspection { source: ArtifactError::Io(_), evidence }) if evidence.stdout == b"service : {}\n")
);
}
#[test]
fn grammar_and_expected_service_policy_remain_with_the_consumer() {
let fixture = Fixture::new();
let source = fixture.source();
let environment = [(
OsString::from("FIXTURE_CANDID_MODE"),
OsString::from("empty"),
)];
let candid = extract(
&fixture.tool(),
&source,
&fixture.context(&environment),
8,
LIMITS,
)
.unwrap();
assert_eq!(candid.text, "");
}
#[test]
fn source_paths_with_spaces_shell_characters_and_non_utf8_are_literal_arguments() {
let fixture = Fixture::new();
let tool = fixture.tool();
let mut name = b"source $(must-not-execute) space ".to_vec();
name.push(0xff);
name.extend_from_slice(b".wasm");
let source = fixture.root.join(OsString::from_vec(name));
fs::write(&source, b"\0asm\x01\0\0\0").unwrap();
let candid = extract(&tool, &source, &fixture.context(&[]), 8, LIMITS).unwrap();
assert_eq!(candid.source_identity.bytes, 8);
assert_eq!(fs::read(&source).unwrap(), b"\0asm\x01\0\0\0");
}