use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
use std::process::Output;
use regex::Regex;
struct Normalize {
pattern: Regex,
replacement: String,
}
struct Directives {
aux_builds: Vec<String>,
normalize_stderr: Vec<Normalize>,
}
#[test]
fn ui_deny_heap_allocations_in_onchain_instruction_handlers() {
run_ui_tests("deny_heap_allocations_in_onchain_instruction_handlers");
}
#[test]
fn ui_require_associated_token_address_before_ata_cast() {
run_ui_tests("require_associated_token_address_before_ata_cast");
}
#[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_owner_before_token_cast() {
run_ui_tests("require_owner_before_token_cast");
}
#[test]
fn ui_require_program_owned_before_lamport_mutation() {
run_ui_tests("require_program_owned_before_lamport_mutation");
}
#[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_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_empty_before_init() {
run_ui_tests("require_empty_before_init");
}
#[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());
}
}
}
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 directives = Directives {
aux_builds: Vec::new(),
normalize_stderr: Vec::new(),
};
for line in source.lines() {
if let Some(name) = line.trim().strip_prefix("// aux-build: ") {
directives.aux_builds.push(name.trim().to_owned());
}
if let Some(rule) = line.trim().strip_prefix("// normalize-stderr-test: ") {
let (pattern, replacement) = rule
.split_once(" -> ")
.unwrap_or_else(|| panic!("invalid normalize-stderr-test rule: {rule}"));
directives.normalize_stderr.push(Normalize {
pattern: Regex::new(&unquote(pattern))
.unwrap_or_else(|source| panic!("invalid normalization pattern: {source}")),
replacement: unquote(replacement),
});
}
}
directives
}
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
}