use std::collections::BTreeMap;
use std::fmt::Write as _;
use std::path::PathBuf;
use std::sync::Mutex;
use super::spawn_from_stable_dir;
const CENSUS_DIR_NAME: &str = "toolchain-census";
pub(crate) struct ToolchainGate {
name: &'static str,
binary: &'static str,
version_arg: &'static str,
require_env: &'static str,
}
pub(crate) const GO: ToolchainGate = ToolchainGate {
name: "go",
binary: "go",
version_arg: "version",
require_env: "ALEF_REQUIRE_GO",
};
pub(crate) const SWIFT: ToolchainGate = ToolchainGate {
name: "swift",
binary: "swift",
version_arg: "--version",
require_env: "ALEF_REQUIRE_SWIFT",
};
#[derive(Clone, Copy, Default)]
struct Tally {
attempted: u32,
executed: u32,
skipped: u32,
}
static CENSUS: Mutex<Census> = Mutex::new(Census {
tallies: BTreeMap::new(),
resolved: BTreeMap::new(),
});
struct Census {
tallies: BTreeMap<&'static str, Tally>,
resolved: BTreeMap<&'static str, Option<PathBuf>>,
}
impl ToolchainGate {
pub(crate) fn name(&self) -> &'static str {
self.name
}
pub(crate) fn open(&self) -> Option<PathBuf> {
let resolved = self.resolve();
self.record(resolved.is_some());
self.require_available(resolved, std::env::var_os(self.require_env).is_some())
}
fn require_available(&self, resolved: Option<PathBuf>, required: bool) -> Option<PathBuf> {
assert!(
resolved.is_some() || !required,
"{} is set but `{}` is unavailable: this fixture compiles alef's generated output \
with the real {} toolchain and verifies nothing without it",
self.require_env,
self.binary,
self.name
);
resolved
}
fn resolve(&self) -> Option<PathBuf> {
let mut census = lock();
if let Some(cached) = census.resolved.get(self.name) {
return cached.clone();
}
let resolved = which::which(self.binary).ok().filter(|_| self.is_runnable());
census.resolved.insert(self.name, resolved.clone());
resolved
}
fn is_runnable(&self) -> bool {
spawn_from_stable_dir(self.binary)
.arg(self.version_arg)
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.is_ok_and(|status| status.success())
}
fn record(&self, executed: bool) {
let mut census = lock();
let tally = census.tallies.entry(self.name).or_default();
tally.attempted += 1;
if executed {
tally.executed += 1;
} else {
tally.skipped += 1;
}
let snapshot = census.tallies.clone();
drop(census);
flush(&snapshot);
}
}
fn lock() -> std::sync::MutexGuard<'static, Census> {
CENSUS.lock().unwrap_or_else(|error| error.into_inner())
}
fn flush(tallies: &BTreeMap<&'static str, Tally>) {
let Some(path) = census_file() else { return };
let Some(parent) = path.parent() else { return };
if std::fs::create_dir_all(parent).is_err() {
return;
}
let mut rendered = String::new();
for (name, tally) in tallies {
let _ = writeln!(
rendered,
"{name}\t{}\t{}\t{}",
tally.attempted, tally.executed, tally.skipped
);
}
let _ = std::fs::write(&path, rendered);
}
fn census_file() -> Option<PathBuf> {
let exe = std::env::current_exe().ok()?;
let name = exe.file_stem()?.to_str()?.to_owned();
let target_dir = exe.parent()?.parent()?.parent()?;
Some(target_dir.join(CENSUS_DIR_NAME).join(format!("{name}.tsv")))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn census_file_lands_under_the_cargo_target_directory() {
let path = census_file().expect("census path resolves for a cargo-built test binary");
let parent = path.parent().expect("census file has a parent directory");
assert_eq!(
parent.file_name().and_then(|name| name.to_str()),
Some(CENSUS_DIR_NAME),
"census file must sit in the {CENSUS_DIR_NAME} directory, got {path:?}"
);
assert_eq!(
path.extension().and_then(|extension| extension.to_str()),
Some("tsv"),
"census file must be a TSV the census script can sum, got {path:?}"
);
}
#[test]
fn opening_a_gate_records_exactly_one_attempt() {
let before = tally_of(GO.name());
let resolved = GO.open();
let after = tally_of(GO.name());
assert_eq!(
after.attempted,
before.attempted + 1,
"opening the go gate must record exactly one attempt"
);
assert_eq!(
(after.executed - before.executed, after.skipped - before.skipped),
if resolved.is_some() { (1, 0) } else { (0, 1) },
"an attempt must land in exactly one of the executed/skipped columns"
);
assert_eq!(
after.attempted,
after.executed + after.skipped,
"attempted must always equal executed + skipped, or the census cannot be read as a ratio"
);
}
#[test]
fn the_flushed_row_reports_attempted_executed_and_skipped() {
let _ = GO.open();
let path = census_file().expect("census path resolves");
let rendered = std::fs::read_to_string(&path).expect("census file was flushed to disk");
let row = rendered
.lines()
.find(|line| line.starts_with(&format!("{}\t", GO.name())))
.unwrap_or_else(|| panic!("no `{}` row in flushed census:\n{rendered}", GO.name()));
let columns: Vec<&str> = row.split('\t').collect();
assert_eq!(
columns.len(),
4,
"a census row is <toolchain>\\t<attempted>\\t<executed>\\t<skipped>, got {row:?}"
);
let attempted: u32 = columns[1].parse().expect("attempted column is a number");
let executed: u32 = columns[2].parse().expect("executed column is a number");
let skipped: u32 = columns[3].parse().expect("skipped column is a number");
assert!(
attempted > 0,
"the row must record the attempt that just happened: {row:?}"
);
assert_eq!(attempted, executed + skipped, "flushed row does not add up: {row:?}");
}
#[test]
fn required_mode_fails_when_the_toolchain_is_unavailable() {
let result = std::panic::catch_unwind(|| GO.require_available(None, true));
let panic = result.expect_err("required mode must fail when the toolchain is unavailable");
let message = panic
.downcast_ref::<String>()
.map(String::as_str)
.or_else(|| panic.downcast_ref::<&str>().copied())
.unwrap_or("non-string panic");
assert!(
message.contains("ALEF_REQUIRE_GO is set"),
"the panic must name the variable that made the toolchain required, got: {message}"
);
}
#[test]
fn unrequired_mode_reports_an_absent_toolchain_as_a_skip() {
assert_eq!(
GO.require_available(None, false),
None,
"an absent, unrequired toolchain must be reported as not-run rather than panicking"
);
}
fn tally_of(name: &'static str) -> Tally {
lock().tallies.get(name).copied().unwrap_or_default()
}
}