use std::process::Command;
use anyhow::{Context, Result, ensure};
use camino::{Utf8Path, Utf8PathBuf};
use cap_std::{ambient_authority, fs_utf8::Dir};
use tempfile::TempDir;
const FIXTURE_SOURCE: &str = include_str!("data/env_access/prohibited.rs.txt");
const TOOLCHAIN: &str = include_str!("../rust-toolchain.toml");
const EXPECTED_DIAGNOSTICS: [(&str, &str); 6] = [
("std::env::var", "inject an environment reader"),
("std::env::var_os", "inject an environment reader"),
("std::env::vars", "inject an environment reader"),
("std::env::vars_os", "inject an environment reader"),
("std::env::set_var", "use a stub environment in tests"),
("std::env::remove_var", "use a stub environment in tests"),
];
const FIXTURE_MANIFEST: &str = concat!(
"[package]\n",
"name = \"env-access-fixture\"\n",
"version = \"0.0.0\"\n",
"edition = \"2024\"\n",
"publish = false\n",
"\n",
"[workspace]\n",
"\n",
"[lib]\n",
"path = \"lib.rs\"\n",
);
fn utf8(path: &std::path::Path) -> Result<&Utf8Path> {
Utf8Path::from_path(path).context("temporary directory path should be UTF-8")
}
fn repository_root() -> Utf8PathBuf { Utf8PathBuf::from(env!("CARGO_MANIFEST_DIR")) }
fn lint_the_fixture() -> Result<String> {
let directory = TempDir::new().context("create a directory for the fixture package")?;
let root = utf8(directory.path())?.to_owned();
let handle = Dir::open_ambient_dir(&root, ambient_authority())
.with_context(|| format!("open the fixture directory {root}"))?;
handle
.write("Cargo.toml", FIXTURE_MANIFEST)
.context("write the fixture manifest")?;
handle
.write("lib.rs", FIXTURE_SOURCE)
.context("write the fixture source")?;
handle
.write("rust-toolchain.toml", TOOLCHAIN)
.context("write the fixture toolchain file")?;
let output = Command::new(env!("CARGO"))
.arg("clippy")
.arg("--manifest-path")
.arg(root.join("Cargo.toml"))
.env("CLIPPY_CONF_DIR", repository_root())
.env("CARGO_TARGET_DIR", root.join("target"))
.env("CARGO_BUILD_BUILD_DIR", root.join("build"))
.env_remove("RUSTFLAGS")
.env_remove("CARGO_ENCODED_RUSTFLAGS")
.env_remove("RUSTDOCFLAGS")
.env_remove("CARGO_ENCODED_RUSTDOCFLAGS")
.env_remove("LLVM_PROFILE_FILE")
.env_remove("CLIPPY_ARGS")
.output()
.context("run Clippy over the fixture package")?;
drop(directory);
String::from_utf8(output.stderr).context("Clippy output should be UTF-8")
}
const DIAGNOSTIC_MARKER: &str = "use of a disallowed method";
fn diagnostics(stderr: &str) -> Vec<&str> {
let mut found = Vec::new();
let mut rest = stderr;
while let Some(start) = rest.find(DIAGNOSTIC_MARKER) {
rest = &rest[start..];
let end = rest[DIAGNOSTIC_MARKER.len()..]
.find(DIAGNOSTIC_MARKER)
.map_or(rest.len(), |offset| offset + DIAGNOSTIC_MARKER.len());
found.push(&rest[..end]);
rest = &rest[end..];
}
found
}
#[test]
fn the_lint_fires_for_every_prohibited_method() -> Result<()> {
let stderr = lint_the_fixture()?;
let reported = diagnostics(&stderr);
for (method, reason) in EXPECTED_DIAGNOSTICS {
let named = format!("`{method}`");
let matched: Vec<&&str> = reported
.iter()
.filter(|diagnostic| diagnostic.contains(&named))
.collect();
ensure!(
matched.len() == 1,
"{method} should be reported once, found {} diagnostics in:\n{stderr}",
matched.len()
);
ensure!(
matched[0].contains(reason),
"the {method} diagnostic should carry the reason `{reason}`, found:\n{}",
matched[0]
);
}
ensure!(
reported.len() == EXPECTED_DIAGNOSTICS.len(),
"expected {} disallowed-method diagnostics, found {} in:\n{stderr}",
EXPECTED_DIAGNOSTICS.len(),
reported.len()
);
Ok(())
}