use std::{error::Error, process::Command};
use expect_test::expect;
use proptest::prelude::*;
use super::*;
#[test]
fn rendering_is_deterministic_and_keeps_ignored_checks_visible() -> Result<(), BehaviorError> {
let inventory = BehaviorInventory::from_listings(
"tests::z_last_check: test\ntests::équality_is_preserved: test\ntests::a_first_check: test\n",
"tests::z_last_check: test\n",
)?;
expect![[r#"
- A first check
- Z last check [ignored]
- Équality is preserved
"#]]
.assert_eq(&inventory.render());
assert_eq!(inventory.checks().len(), 3);
assert_eq!(
inventory.checks().first().map(BehaviorCheck::disposition),
Some(CheckDisposition::Runnable)
);
Ok(())
}
#[test]
fn equal_sentences_retain_their_distinct_code_locations() -> Result<(), BehaviorError> {
let inventory = BehaviorInventory::from_listings(
"east::ownership_is_preserved: test\nwest::ownership_is_preserved: test\n",
"",
)?;
expect![[r#"
- Ownership is preserved (east::ownership_is_preserved)
- Ownership is preserved (west::ownership_is_preserved)
"#]]
.assert_eq(&inventory.render());
Ok(())
}
#[test]
fn snapshot_scoping_respects_module_boundaries_and_excludes_its_own_module()
-> Result<(), BehaviorError> {
let listing = "machine::tests::preserves_state: test\nmachine::behavior::inventory: test\nmachine::behavior::nested::helper: test\nmachine_extra::unrelated: test\nroot_check: test\n";
let inventory = BehaviorInventory::from_listings(listing, "")?
.beside_snapshot("consumer::machine::behavior")?;
assert_eq!(
inventory
.checks()
.iter()
.map(BehaviorCheck::qualified_name)
.collect::<Vec<_>>(),
["machine::tests::preserves_state"]
);
let root =
BehaviorInventory::from_listings(listing, "")?.beside_snapshot("consumer::behavior")?;
assert_eq!(root.checks().len(), 5);
Ok(())
}
#[test]
fn malformed_duplicate_or_inconsistent_listings_are_refused() {
for line in ["unexpected output", ": test", "a::::check: test"] {
assert!(matches!(BehaviorInventory::from_listings(line, ""),
Err(BehaviorError::InvalidListing { line: rejected }) if rejected == line));
}
assert!(
matches!(BehaviorInventory::from_listings("check: test\ncheck: test", ""),
Err(BehaviorError::DuplicateCheck { name }) if name == "check")
);
assert!(
matches!(BehaviorInventory::from_listings("check: test", "other: test"),
Err(BehaviorError::UnknownIgnoredCheck { name }) if name == "other")
);
}
#[test]
fn absent_or_malformed_snapshot_scopes_are_refused() -> Result<(), BehaviorError> {
for module in [
"consumer",
"consumer::",
"::behavior",
"consumer::machine::::behavior",
] {
let inventory = BehaviorInventory::from_listings("check: test", "")?;
assert!(matches!(inventory.beside_snapshot(module),
Err(BehaviorError::InvalidSnapshotModule { module: rejected }) if rejected == module));
}
assert!(matches!(
BehaviorInventory::from_listings("other::check: test", "")?
.beside_snapshot("consumer::machine::behavior"),
Err(BehaviorError::EmptyScope { .. })
));
Ok(())
}
#[test]
fn benchmark_functions_are_not_behavior_checks() -> Result<(), BehaviorError> {
let inventory = BehaviorInventory::from_listings("\nspeed: benchmark\ncheck: test\n", "")?;
assert_eq!(inventory.render(), "- Check\n");
assert_eq!(BehaviorInventory::from_listings("", "")?.render(), "");
Ok(())
}
macro_rules! generated_check {
($name:ident) => {
#[test]
fn $name() -> Result<(), BehaviorError> {
let inventory = BehaviorInventory::from_listings("generated: test", "")?;
assert_eq!(inventory.render(), "- Generated\n");
Ok(())
}
};
}
generated_check!(macro_generated_checks_remain_discoverable);
#[test]
#[ignore = "Fixture for the compiler's ignored-check listing; not a verified claim"]
fn an_ignored_inventory_fixture() {}
#[cfg(any())]
#[test]
fn an_uncompiled_inventory_fixture() {}
#[test]
fn compiled_discovery_includes_generated_checks_and_preserves_ignored_status()
-> Result<(), Box<dyn Error>> {
let inventory = BehaviorInventory::from_test_binary(env::current_exe()?)?;
let check = |name| {
inventory
.checks()
.iter()
.find(|check| check.qualified_name() == name)
};
assert_eq!(
check("behavior::tests::macro_generated_checks_remain_discoverable")
.map(BehaviorCheck::disposition),
Some(CheckDisposition::Runnable)
);
assert_eq!(
check("behavior::tests::an_ignored_inventory_fixture").map(BehaviorCheck::disposition),
Some(CheckDisposition::Ignored)
);
assert_eq!(
check("behavior::tests::an_uncompiled_inventory_fixture"),
None
);
assert_eq!(
check("behavior::tests::properties::listing_order_does_not_change_the_inventory")
.map(BehaviorCheck::disposition),
Some(CheckDisposition::Runnable)
);
Ok(())
}
#[test]
fn configured_runners_preserve_scope_filters_and_ignored_checks() -> Result<(), Box<dyn Error>> {
let mut runner = Command::new(env::current_exe()?);
runner.arg("behavior::tests::");
let inventory = BehaviorInventory::for_snapshot_with_runner(
"pipecircuit_visualization::behavior::tests::snapshot",
runner,
)?;
let ordinary =
BehaviorInventory::for_snapshot("pipecircuit_visualization::behavior::tests::snapshot")?;
assert_eq!(inventory, ordinary);
assert!(
inventory
.checks()
.iter()
.all(|check| { check.qualified_name().starts_with("behavior::tests::") })
);
assert!(inventory.checks().iter().any(|check| {
check.qualified_name() == "behavior::tests::an_ignored_inventory_fixture"
&& check.disposition() == CheckDisposition::Ignored
}));
assert!(inventory.checks().iter().any(|check| {
check.qualified_name() == "behavior::tests::macro_generated_checks_remain_discoverable"
&& check.disposition() == CheckDisposition::Runnable
}));
Ok(())
}
#[test]
fn configured_runner_failures_preserve_the_executable_and_output() -> Result<(), Box<dyn Error>> {
let absent = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("absent-test-runner");
assert!(matches!(
BehaviorInventory::for_snapshot_with_runner("consumer::behavior", Command::new(&absent)),
Err(BehaviorError::DiscoveryIo { executable, source })
if executable == absent && source.kind() == io::ErrorKind::NotFound
));
let executable = env::current_exe()?;
let mut runner = Command::new(&executable);
runner.arg("--unknown-inventory-option");
let expected = runner.args(["--list", "--format", "terse"]).output()?;
let mut runner = Command::new(&executable);
runner.arg("--unknown-inventory-option");
let Err(BehaviorError::DiscoveryFailed {
executable: rejected,
output,
}) = BehaviorInventory::for_snapshot_with_runner("consumer::behavior", runner)
else {
return Err("failed runner discovery was accepted".into());
};
assert_eq!(rejected, executable);
assert_eq!(output.status, expected.status);
assert_eq!(output.stdout, expected.stdout);
assert_eq!(output.stderr, expected.stderr);
Ok(())
}
#[test]
fn configured_runners_refuse_missing_or_malformed_snapshot_scopes() -> Result<(), Box<dyn Error>> {
let executable = env::current_exe()?;
let runner = || {
let mut command = Command::new(&executable);
command.arg("behavior::tests::");
command
};
assert!(matches!(
BehaviorInventory::for_snapshot_with_runner("consumer", runner()),
Err(BehaviorError::InvalidSnapshotModule { module }) if module == "consumer"
));
assert!(matches!(
BehaviorInventory::for_snapshot_with_runner("consumer::absent::behavior", runner()),
Err(BehaviorError::EmptyScope { module }) if module == "consumer::absent::behavior"
));
Ok(())
}
#[test]
fn process_discovery_failures_retain_their_original_details() -> Result<(), Box<dyn Error>> {
let absent = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("absent-test-executable");
assert!(matches!(BehaviorInventory::from_test_binary(&absent),
Err(BehaviorError::DiscoveryIo { executable, source })
if executable == absent && source.kind() == io::ErrorKind::NotFound));
let failed = Command::new(env::current_exe()?)
.arg("--unknown-inventory-option")
.output()?;
assert!(!failed.status.success());
let executable = env::current_exe()?;
let Err(failure @ BehaviorError::DiscoveryFailed { .. }) =
test_listing(&executable, failed.clone())
else {
return Err("failed discovery was accepted".into());
};
assert!(failure.to_string().contains("failed with"));
let BehaviorError::DiscoveryFailed {
executable: retained,
output,
} = failure
else {
return Err("discovery failure changed kind".into());
};
assert_eq!(retained, executable);
assert_eq!(output.status, failed.status);
assert_eq!(output.stdout, failed.stdout);
assert_eq!(output.stderr, failed.stderr);
let mut invalid_listing = Command::new(&executable)
.args(["--list", "--format", "terse"])
.output()?;
assert!(invalid_listing.status.success());
invalid_listing.stdout = vec![255];
let Err(BehaviorError::ListingUtf8(rejected)) = test_listing(&executable, invalid_listing)
else {
return Err("invalid listing bytes were accepted".into());
};
assert_eq!(rejected.into_bytes(), [255]);
let Err(invalid_utf8) = String::from_utf8(vec![255]) else {
return Err("invalid UTF-8 was accepted".into());
};
let errors = [
BehaviorError::CurrentExecutable(io::Error::from(io::ErrorKind::NotFound)),
BehaviorError::DiscoveryIo {
executable: absent,
source: io::Error::from(io::ErrorKind::NotFound),
},
BehaviorError::ListingUtf8(invalid_utf8),
BehaviorError::InvalidListing {
line: "unknown".to_owned(),
},
BehaviorError::DuplicateCheck {
name: "check".to_owned(),
},
BehaviorError::UnknownIgnoredCheck {
name: "check".to_owned(),
},
BehaviorError::InvalidSnapshotModule {
module: "invalid".to_owned(),
},
BehaviorError::EmptyScope {
module: "empty".to_owned(),
},
BehaviorError::RecursiveDiscovery,
];
for error in errors {
assert!(!error.to_string().is_empty());
}
Ok(())
}
mod properties {
use super::*;
proptest! {
#[test]
fn listing_order_does_not_change_the_inventory(
names in prop::collection::btree_set("[a-z][a-z_]{0,24}", 0..30),
) {
let forward = names.iter().map(|name| format!("tests::{name}: test\n")).collect::<String>();
let reverse = names.iter().rev().map(|name| format!("tests::{name}: test\n")).collect::<String>();
let forward = BehaviorInventory::from_listings(&forward, "")
.map_err(|error| TestCaseError::fail(error.to_string()))?;
let reverse = BehaviorInventory::from_listings(&reverse, "")
.map_err(|error| TestCaseError::fail(error.to_string()))?;
prop_assert_eq!(&forward, &reverse);
prop_assert_eq!(forward.render(), reverse.render());
}
}
}