#![allow(clippy::bool_assert_comparison)]
use fs4::fs_std::FileExt;
use std::ffi::OsString;
use std::fs::File;
use std::path::{Path, PathBuf};
use std::process::Command;
fn project_path(dir: &str) -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests").join(dir)
}
fn path_count_for(output: &str, fn_name: &str) -> usize {
let header = format!("Function: \"{}\":", fn_name);
let mut in_block = false;
let mut count = 0;
for line in output.lines() {
if line.contains(&header) {
in_block = true;
continue;
}
if in_block {
if line.contains("Function:") {
break;
}
if line.trim().starts_with("Path [") {
count += 1;
}
}
}
count
}
struct LockGuard {
file: std::fs::File,
path: PathBuf,
}
impl LockGuard {
fn new(path: PathBuf) -> Self {
let file = File::create(&path).expect("Failed to create lock file");
file.lock_exclusive().expect("Failed to acquire lock");
Self { file, path }
}
}
impl Drop for LockGuard {
fn drop(&mut self) {
let _ = self.file.unlock();
let _ = std::fs::remove_file(&self.path);
}
}
#[inline(always)]
fn run_with_args(dir: &str, args: &[&str]) -> String {
let project_path = project_path(dir);
let _lock = LockGuard::new(project_path.join(".rapx-test.lock"));
let mut command = cargo_rapx_command();
let output = command
.args(args)
.current_dir(&project_path)
.output()
.expect("Failed to execute cargo rapx");
String::from_utf8_lossy(&output.stderr).into_owned()
}
fn cargo_rapx_command() -> Command {
if let Some(path) = option_env!("CARGO_BIN_EXE_cargo-rapx") {
let path = PathBuf::from(path);
let mut command = Command::new(&path);
command.arg("rapx");
prepend_local_bin_to_path(&mut command, path.parent());
return command;
}
let mut command = Command::new("cargo");
command.arg("rapx");
command
}
fn prepend_local_bin_to_path(command: &mut Command, cargo_rapx_dir: Option<&Path>) {
let local_rapx_dir = option_env!("CARGO_BIN_EXE_rapx")
.and_then(|path| PathBuf::from(path).parent().map(Path::to_path_buf))
.or_else(|| cargo_rapx_dir.map(Path::to_path_buf));
let Some(local_rapx_dir) = local_rapx_dir else {
return;
};
let mut paths = vec![local_rapx_dir];
if let Some(path) = std::env::var_os("PATH") {
paths.extend(std::env::split_paths(&path));
}
if let Ok(path) = std::env::join_paths(paths) {
command.env("PATH", OsString::from(path));
}
}
fn assert_contain(output: &str, pattern: &str) {
assert!(
output.contains(pattern),
"Missing pattern:\n{}\nFull output:\n{}",
pattern,
output
);
}
fn assert_not_contain(output: &str, pattern: &str) {
assert!(
!output.contains(pattern),
"Unexpected pattern:\n{}\nFull output:\n{}",
pattern,
output
);
}
fn assert_unproved_exclusive(output: &str, function: &str, allowed: &[&str]) {
assert_unproved_exclusive_with_result(output, function, allowed, "UNSOUND");
}
fn assert_function_result(output: &str, function: &str, result_pat: &str) {
assert_contain(output, &format!("function: {function}"));
let block = extract_block_after(output, &format!("function: {function}"));
assert!(
block.contains(&format!("result: {result_pat}")),
"Expected result: {result_pat} for {function}\nBlock:\n{block}"
);
}
fn assert_unproved_exclusive_with_result(
output: &str,
function: &str,
allowed: &[&str],
result_pat: &str,
) {
assert_contain(output, &format!("function: {function}"));
let block = extract_block_after(output, &format!("function: {function}"));
if let Some(primary) = allowed.first() {
let matches_plain = block.contains(&format!("{primary} | Failed"))
|| block.contains(&format!("{primary} | Unknown"));
let matches_hazard = block.contains(&format!("[hazard] {primary} | Failed"))
|| block.contains(&format!("[hazard] {primary} | Unknown"));
let matches_option = block.contains(&format!("[option] {primary} | Failed"))
|| block.contains(&format!("[option] {primary} | Unknown"));
assert!(
matches_plain || matches_hazard || matches_option,
"Expected {primary} | Failed/Unknown for {function}\nBlock:\n{block}"
);
}
let mut actual: Vec<&str> = Vec::new();
for line in block.lines() {
for sfx in ["| Failed", "| Unknown"] {
let Some(idx) = line.find(sfx) else { continue };
let prefix = line[..idx].trim_end();
if prefix.contains("[hazard]") || prefix.contains("[option]") {
continue;
}
let prop = prefix.rsplit(' ').next().unwrap_or("");
if !prop.is_empty() && prop != "Unknown" {
actual.push(prop);
}
}
}
actual.sort();
actual.dedup();
let unexpected: Vec<_> = actual.iter().filter(|p| !allowed.contains(p)).collect();
assert!(
unexpected.is_empty(),
"Unexpected Failed/Unknown for {function}: {unexpected:?}\nAllowed: {allowed:?}\nBlock:\n{block}"
);
assert_contain(output, &format!("result: {result_pat}"));
}
fn extract_block_after<'a>(text: &'a str, marker: &str) -> &'a str {
let Some(pos) = text.find(marker) else {
return "";
};
let rest = &text[pos + marker.len()..];
match rest.find("function: ") {
Some(end) => &rest[..end],
None => rest,
}
}
const CMD_CHECK_UAF: &[&str] = &["check", "-f"];
const CMD_CHECK_MEMLEAK: &[&str] = &["check", "-m"];
const CMD_ANALYZE_ALIAS: &[&str] = &["analyze", "alias"];
const CMD_ANALYZE_ALIAS_MFP: &[&str] = &["analyze", "alias", "--strategy", "mfp"];
const CMD_ANALYZE_OWNEDHEAP: &[&str] = &["analyze", "ownedheap"];
const CMD_ANALYZE_PATHS: &[&str] = &["analyze", "paths"];
const CMD_ANALYZE_PATHS_REPEAT_1: &[&str] = &["analyze", "paths", "--postfix-repeat", "1"];
const CMD_ANALYZE_PATHS_REPEAT_2: &[&str] = &["analyze", "paths", "--postfix-repeat", "2"];
const CMD_ANALYZE_SAFETYFLOW: &[&str] = &["analyze", "safetyflow"];
const CMD_ANALYZE_SSA: &[&str] = &["analyze", "ssa"];
const CMD_ANALYZE_RANGE: &[&str] = &["analyze", "range"];
const CMD_ANALYZE_CALLGRAPH: &[&str] = &["analyze", "callgraph"];
const CMD_ANALYZE_ADG: &[&str] = &["analyze", "adg", "--dump", "api_graph.yml"];
const CMD_VERIFY: &[&str] = &["verify"];
const CMD_VERIFY_TARGETED: &[&str] = &["verify", "--mode", "targeted"];
const CMD_VERIFY_PREPARE: &[&str] = &["verify", "--prepare-targets"];
const CMD_VERIFY_REPEAT_1: &[&str] = &["verify", "--postfix-repeat", "1"];
const CMD_VERIFY_REPEAT_2: &[&str] = &["verify", "--postfix-repeat", "2"];
const CMD_VERIFY_SCAN: &[&str] = &["verify", "--mode", "scan"];
const CMD_VERIFY_SKIP_INVARIANT: &[&str] = &["verify", "--skip-invariant"];
macro_rules! verify_sound {
($dir:literal, $func:literal) => {{
let output = $crate::run_with_args($dir, CMD_VERIFY);
$crate::assert_contain(&output, concat!("function: ", $func));
$crate::assert_contain(&output, "result: SOUND");
}};
}
macro_rules! verify_unsound {
($dir:literal, $func:literal, $prop:literal) => {{
let output = $crate::run_with_args($dir, CMD_VERIFY);
$crate::assert_unproved_exclusive(&output, $func, &[$prop]);
}};
}
macro_rules! verify_hazard {
($dir:literal, $func:literal, $prop:literal) => {{
let output = $crate::run_with_args($dir, CMD_VERIFY);
$crate::assert_unproved_exclusive_with_result(&output, $func, &[$prop], "HAZARD");
}};
}
include!("suites/check.rs");
include!("suites/analyze.rs");
include!("suites/verify_units.rs");
include!("suites/verify_cases.rs");
include!("suites/opt.rs");
#[test]
fn all_fixture_dirs_tested() {
let all_sources = [
include_str!("suites/check.rs"),
include_str!("suites/analyze.rs"),
include_str!("suites/verify_units.rs"),
include_str!("suites/verify_cases.rs"),
include_str!("suites/opt.rs"),
]
.concat();
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests");
let categories: &[&str] = &["check", "analyze", "verify_units", "verify_cases", "opt"];
let mut missing = Vec::new();
for cat in categories {
let cat_dir = root.join(cat);
if !cat_dir.is_dir() {
continue;
}
for entry in std::fs::read_dir(&cat_dir).expect("read dir failed") {
let entry = entry.expect("read entry failed");
let name = entry.file_name();
let dir_path = format!("{}/{}", cat, name.to_str().unwrap());
if !entry.file_type().expect("file_type failed").is_dir() {
continue;
}
if !all_sources.contains(&dir_path) {
missing.push(dir_path);
}
}
}
assert!(
missing.is_empty(),
"Fixture directories not referenced in any test:\n {}",
missing.join("\n ")
);
}
#[test]
fn std_contracts_valid() {
let json = std::fs::read_to_string(
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("src/verify/source/assets/std-public-contracts.json"),
)
.expect("failed to read contracts JSON");
let db: std::collections::HashMap<String, Vec<serde_json::Value>> =
serde_json::from_str(&json).expect("failed to parse contracts JSON");
for (key, entries) in &db {
for entry in entries {
assert!(entry["tag"].is_string(), "{key}: missing or invalid tag");
assert!(entry["args"].is_array(), "{key}: missing or invalid args");
}
}
assert!(!db.is_empty(), "contract database is empty");
}