use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
use std::process::Output;
use regex::Regex;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum ExpectedOutcome {
CompileFail,
CheckWarn,
CheckPass,
}
struct Normalize {
pattern: Regex,
replacement: String,
}
struct Directives {
aux_builds: Vec<String>,
normalize_stderr: Vec<Normalize>,
expected_outcome: ExpectedOutcome,
}
#[test]
fn ui_deny_heap_allocations_in_onchain_instruction_handlers() {
run_ui_tests("deny_heap_allocations_in_onchain_instruction_handlers");
}
#[test]
fn ui_deny_unchecked_remaining_mut() {
run_ui_tests("deny_unchecked_remaining_mut");
}
#[test]
fn ui_require_canonical_instruction_dispatch_for_idl() {
run_ui_tests("require_canonical_instruction_dispatch_for_idl");
}
#[test]
fn ui_require_idl_root_to_define_one_program_id() {
run_ui_tests("require_idl_root_to_define_one_program_id");
}
#[test]
fn ui_require_sysvar_assert_before_sysvar_use() {
run_ui_tests("require_sysvar_assert_before_sysvar_use");
}
#[test]
fn ui_require_type_assert_before_zero_copy_cast() {
run_ui_tests("require_type_assert_before_zero_copy_cast");
}
#[test]
fn ui_require_zeroed_before_close() {
run_ui_tests("require_zeroed_before_close");
}
#[test]
fn ui_deny_account_borrows_across_cpi() {
run_ui_tests("deny_account_borrows_across_cpi");
}
#[test]
fn ui_deny_unused_account_borrow_guards() {
run_ui_tests("deny_unused_account_borrow_guards");
}
#[test]
fn ui_require_bounded_remaining_accounts() {
run_ui_tests("require_bounded_remaining_accounts");
}
#[test]
fn ui_require_canonical_bump_before_pda_write() {
run_ui_tests("require_canonical_bump_before_pda_write");
}
#[test]
fn ui_require_checked_asset_arithmetic() {
run_ui_tests("require_checked_asset_arithmetic");
}
#[test]
fn ui_require_consistent_token_program() {
run_ui_tests("require_consistent_token_program");
}
#[test]
fn ui_require_explicit_discriminators_and_seed_namespaces() {
run_ui_tests("require_explicit_discriminators_and_seed_namespaces");
}
#[test]
fn ui_require_explicit_token_2022_extension_policy() {
run_ui_tests("require_explicit_token_2022_extension_policy");
}
#[test]
fn ui_require_post_cpi_balance_reload() {
run_ui_tests("require_post_cpi_balance_reload");
}
#[test]
fn ui_require_program_check_before_cpi() {
run_ui_tests("require_program_check_before_cpi");
}
#[test]
fn ui_require_reason_for_duplicate_remaining_accounts() {
run_ui_tests("require_reason_for_duplicate_remaining_accounts");
}
#[test]
fn ui_require_writable_before_account_resize() {
run_ui_tests("require_writable_before_account_resize");
}
fn run_ui_tests(lint: &str) {
let src_base = test_root().join(lint);
let driver = driver_path();
let mut fixtures = collect_fixtures(&src_base);
assert!(
!fixtures.is_empty(),
"no fixtures found under {}",
src_base.display()
);
fixtures.sort();
for fixture in fixtures {
let stem = fixture
.file_stem()
.expect("fixture has a file stem")
.to_string_lossy()
.to_string();
let expected_path = fixture.with_extension("stderr");
let expected = read_to_string(&expected_path);
let directives = parse_directives(&fixture);
let aux = build_auxiliaries(&driver, &src_base, &directives);
let output = compile_fixture(&driver, &fixture, &aux, lint);
let actual = normalize_stderr(
String::from_utf8_lossy(&output.stderr).to_string(),
&src_base,
&directives,
);
if actual != expected {
let actual_path = std::env::temp_dir()
.join("pina-lints-ui")
.join(lint)
.join(format!("{stem}.stderr"));
std::fs::create_dir_all(actual_path.parent().expect("parent directory"))
.expect("could not create actual-stderr directory");
std::fs::write(&actual_path, &actual).expect("could not save actual stderr");
println!(
"stderr mismatch for {}\n--- expected ({})\n{expected}\n--- actual\n{actual}\n--- \
actual saved to {}",
fixture.display(),
expected_path.display(),
actual_path.display(),
);
panic!("stderr mismatch for {}", fixture.display());
}
assert_expected_outcome(&fixture, &output, &expected, directives.expected_outcome);
}
}
fn assert_expected_outcome(
fixture: &Path,
output: &Output,
expected_stderr: &str,
expected_outcome: ExpectedOutcome,
) {
match expected_outcome {
ExpectedOutcome::CompileFail => {
assert!(
!expected_stderr.is_empty(),
"compile-fail fixture {} must expect diagnostics",
fixture.display(),
);
assert!(
!output.status.success(),
"compile-fail fixture {} compiled successfully",
fixture.display(),
);
}
ExpectedOutcome::CheckWarn => {
assert!(
!expected_stderr.is_empty(),
"check-warn fixture {} must expect diagnostics",
fixture.display(),
);
assert!(
output.status.success(),
"check-warn fixture {} failed to compile",
fixture.display(),
);
}
ExpectedOutcome::CheckPass => {
assert!(
expected_stderr.is_empty(),
"check-pass fixture {} must not expect diagnostics",
fixture.display(),
);
assert!(
output.status.success(),
"check-pass fixture {} failed to compile",
fixture.display(),
);
}
}
}
fn test_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("ui")
}
fn driver_path() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_pina_lint_driver"))
}
fn collect_fixtures(src_base: &Path) -> Vec<PathBuf> {
let entries = std::fs::read_dir(src_base)
.unwrap_or_else(|source| panic!("could not read {}: {source}", src_base.display()));
entries
.filter_map(|entry| {
let path = entry.expect("directory entry").path();
(path.extension().is_some_and(|extension| extension == "rs")).then_some(path)
})
.collect()
}
fn read_to_string(path: &Path) -> String {
std::fs::read_to_string(path)
.unwrap_or_else(|source| panic!("could not read {}: {source}", path.display()))
}
fn parse_directives(fixture: &Path) -> Directives {
let source = read_to_string(fixture);
let mut aux_builds = Vec::new();
let mut normalize_stderr = Vec::new();
let mut expected_outcome = None;
for line in source.lines() {
let line = line.trim();
let outcome = match line {
"// compile-fail" => Some(ExpectedOutcome::CompileFail),
"// check-warn" => Some(ExpectedOutcome::CheckWarn),
"// check-pass" => Some(ExpectedOutcome::CheckPass),
_ => None,
};
if let Some(outcome) = outcome {
assert!(
expected_outcome.replace(outcome).is_none(),
"fixture {} declares more than one expected outcome",
fixture.display(),
);
}
if let Some(name) = line.strip_prefix("// aux-build: ") {
aux_builds.push(name.trim().to_owned());
}
if let Some(rule) = line.strip_prefix("// normalize-stderr-test: ") {
let (pattern, replacement) = rule
.split_once(" -> ")
.unwrap_or_else(|| panic!("invalid normalize-stderr-test rule: {rule}"));
normalize_stderr.push(Normalize {
pattern: Regex::new(&unquote(pattern))
.unwrap_or_else(|source| panic!("invalid normalization pattern: {source}")),
replacement: unquote(replacement),
});
}
}
let expected_outcome = expected_outcome.unwrap_or_else(|| {
panic!(
"fixture {} must declare compile-fail, check-warn, or check-pass",
fixture.display(),
)
});
Directives {
aux_builds,
normalize_stderr,
expected_outcome,
}
}
fn unquote(value: &str) -> String {
value.trim().trim_matches('"').to_owned()
}
fn build_auxiliaries(driver: &Path, src_base: &Path, directives: &Directives) -> Vec<String> {
let mut extern_args = Vec::new();
for name in &directives.aux_builds {
let source = src_base.join("auxiliary").join(name);
let crate_name = Path::new(name)
.file_stem()
.expect("auxiliary has a file stem")
.to_string_lossy()
.to_string();
let output_dir = std::env::temp_dir()
.join("pina-lints-ui")
.join(src_base.file_name().expect("lint dir has a name"))
.join("auxiliary");
std::fs::create_dir_all(&output_dir)
.unwrap_or_else(|source| panic!("could not create {}: {source}", output_dir.display()));
let aux_source = read_to_string(&source);
let has_crate_type = aux_source
.lines()
.any(|line| line.trim().starts_with("#![crate_type"));
let compile_flags = aux_source
.lines()
.filter_map(|line| line.trim().strip_prefix("// compile-flags: "))
.collect::<Vec<_>>()
.join(" ");
let mut command = Command::new(driver);
command
.arg("--edition=2024")
.arg("--crate-name")
.arg(&crate_name);
if !has_crate_type {
command.arg("--crate-type=lib");
}
if !compile_flags.is_empty() {
for flag in compile_flags.split_whitespace() {
command.arg(flag);
}
}
command
.arg("--cap-lints")
.arg("allow")
.arg("--out-dir")
.arg(&output_dir)
.arg(&source);
let status = command
.status()
.unwrap_or_else(|source| panic!("could not run driver: {source}"));
assert!(status.success(), "auxiliary crate {name} failed to compile");
let artifact = find_artifact(&output_dir, &crate_name);
extern_args.push("--extern".to_owned());
extern_args.push(format!("{}={}", crate_name, artifact.display()));
}
extern_args
}
fn find_artifact(output_dir: &Path, crate_name: &str) -> PathBuf {
for file_name in [
format!("lib{crate_name}.rlib"),
format!("lib{crate_name}.dylib"),
format!("lib{crate_name}.so"),
] {
let candidate = output_dir.join(&file_name);
if candidate.is_file() {
return candidate;
}
}
panic!(
"could not find compiled auxiliary crate {crate_name} in {}",
output_dir.display()
)
}
fn compile_fixture(driver: &Path, fixture: &Path, extern_args: &[String], lint: &str) -> Output {
Command::new(driver)
.arg("--edition=2024")
.arg("--crate-type=lib")
.arg("--emit=metadata")
.arg("-Zui-testing")
.env("PINA_LINT_ONLY", lint)
.args(extern_args)
.arg(fixture)
.output()
.unwrap_or_else(|source| panic!("could not run driver: {source}"))
}
fn normalize_stderr(mut actual: String, src_base: &Path, directives: &Directives) -> String {
let src_base = src_base.to_string_lossy().to_string();
if !src_base.is_empty() {
actual = actual.replace(&src_base, "$DIR");
}
for rule in &directives.normalize_stderr {
actual = rule
.pattern
.replace_all(&actual, rule.replacement.as_str())
.to_string();
}
actual
}