use std::fs;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
const UNFIXED_SOURCE: &str = r#"#![allow(dead_code, unused_variables, private_interfaces)]
use solana_account_view::AccountView;
pub fn dropped_after_binding(account: &mut AccountView) -> u8 {
let guard = account.try_borrow_mut().unwrap();
drop(guard);
0
}
pub fn underscore_binding(account: &AccountView) {
let _guard = account.try_borrow().unwrap();
}
pub fn read_guard(account: &AccountView) -> usize {
let guard = account.try_borrow().unwrap();
guard.len()
}
"#;
const FIXED_SOURCE: &str = r#"#![allow(dead_code, unused_variables, private_interfaces)]
use solana_account_view::AccountView;
pub fn dropped_after_binding(account: &mut AccountView) -> u8 {
let guard = account.try_borrow_mut().unwrap();
drop(guard);
0
}
pub fn underscore_binding(account: &AccountView) {
account.try_borrow().unwrap();
}
pub fn read_guard(account: &AccountView) -> usize {
let guard = account.try_borrow().unwrap();
guard.len()
}
"#;
fn pinned_sysroot() -> PathBuf {
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let workspace_root = manifest
.ancestors()
.nth(2)
.expect("crate manifest must have a workspace root");
let output = Command::new("rustc")
.arg("--print")
.arg("sysroot")
.current_dir(workspace_root)
.output()
.expect("could not resolve the pinned toolchain sysroot");
assert!(
output.status.success(),
"rustc --print sysroot failed: {}",
String::from_utf8_lossy(&output.stderr)
);
let sysroot = PathBuf::from(String::from_utf8_lossy(&output.stdout).trim());
assert!(
sysroot.is_dir(),
"sysroot is missing: {}",
sysroot.display()
);
sysroot
}
fn pinned_toolchain_env() -> Vec<(String, String)> {
let sysroot = pinned_sysroot();
let toolchain_dir = sysroot
.file_name()
.expect("sysroot must end in its toolchain directory")
.to_string_lossy()
.to_string();
let rustup_home = sysroot
.parent()
.and_then(Path::parent)
.expect("sysroot must live under a rustup home")
.display()
.to_string();
let rustc = sysroot.join("bin").join("rustc");
vec![
("RUSTUP_TOOLCHAIN".to_string(), toolchain_dir),
("RUSTUP_HOME".to_string(), rustup_home),
("RUSTC".to_string(), rustc.display().to_string()),
]
}
struct ScratchCrate {
root: PathBuf,
}
impl ScratchCrate {
fn new(name: &str, source: &str) -> Self {
let root = std::env::temp_dir()
.join("pina-lints-fix-tests")
.join(format!("{name}-{}", std::process::id()));
let _ = fs::remove_dir_all(&root);
fs::create_dir_all(root.join("src")).expect("could not create scratch crate");
fs::write(
root.join("Cargo.toml"),
r#"[package]
name = "fix_fixture"
version = "0.0.0"
edition = "2021"
publish = false
[dependencies]
solana-account-view = "=2.0.0"
[workspace]
"#,
)
.expect("could not write Cargo.toml");
fs::write(root.join("src").join("lib.rs"), source).expect("could not write scratch source");
Self { root }
}
fn source(&self) -> String {
fs::read_to_string(self.root.join("src").join("lib.rs"))
.expect("could not read scratch source")
}
fn cargo(&self, args: &[&str]) {
let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
let output = Command::new(&cargo)
.args(args)
.current_dir(&self.root)
.envs(pinned_toolchain_env())
.env(
"RUSTC_WORKSPACE_WRAPPER",
env!("CARGO_BIN_EXE_pina_lint_driver"),
)
.env("PINA_LINT_NO_DEPS", "1")
.env("PINA_LINT_ONLY", "deny_unused_account_borrow_guards")
.env("CARGO_TARGET_DIR", self.root.join("target"))
.output()
.expect("could not run cargo");
assert!(
output.status.success(),
"cargo {args:?} failed while fixing the scratch crate:\n{}",
String::from_utf8_lossy(&output.stderr)
);
}
fn lint_warnings(&self) -> usize {
let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
let output = Command::new(&cargo)
.args(["check", "--lib", "--offline"])
.current_dir(&self.root)
.envs(pinned_toolchain_env())
.env(
"RUSTC_WORKSPACE_WRAPPER",
env!("CARGO_BIN_EXE_pina_lint_driver"),
)
.env("PINA_LINT_NO_DEPS", "1")
.env("PINA_LINT_ONLY", "deny_unused_account_borrow_guards")
.env("CARGO_TARGET_DIR", self.root.join("target"))
.output()
.expect("could not run cargo check");
assert!(
output.status.success(),
"fixed scratch crate must still compile: {}",
String::from_utf8_lossy(&output.stderr)
);
String::from_utf8_lossy(&output.stderr)
.lines()
.filter(|line| line.contains("account borrow guard"))
.count()
}
}
impl Drop for ScratchCrate {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.root);
}
}
#[test]
fn machine_applicable_fixes_recompile_without_mangling() {
let scratch = ScratchCrate::new("unused-guards", UNFIXED_SOURCE);
scratch.cargo(&[
"fix",
"--lib",
"--offline",
"--allow-dirty",
"--allow-staged",
"--allow-no-vcs",
]);
assert_eq!(
scratch.source(),
FIXED_SOURCE,
"the applied fix mangled the scratch crate"
);
assert_eq!(
scratch.lint_warnings(),
1,
"only the MaybeIncorrect drop-removal warning should remain"
);
}