use std::path::{Path, PathBuf};
pub fn workspace_root_of(manifest_dir: &Path) -> Option<PathBuf> {
let root = manifest_dir.join("../..");
let text = std::fs::read_to_string(root.join("Cargo.toml")).ok()?;
let declares = text
.lines()
.any(|l| l.split('#').next().unwrap_or("").trim() == "[workspace]");
if !declares {
return None;
}
let me = manifest_dir.canonicalize().ok()?;
let listed = root
.join("crates")
.join(me.file_name()?)
.canonicalize()
.ok()?;
if listed != me {
return None;
}
root.canonicalize().ok()
}
pub fn workspace_path_or_skip_at(test: &str, manifest_dir: &Path, rel: &str) -> Option<PathBuf> {
let Some(root) = workspace_root_of(manifest_dir) else {
eprintln!(
"SKIP {test}: out of tree ({} is not a member of the aprender workspace) - {rel} \
lives in the workspace, which a published crate does not carry",
manifest_dir.display()
);
return None;
};
let path = root.join(rel);
assert!(
path.exists(),
"{test}: in tree, {} must exist (only an out-of-tree build may skip)",
path.display()
);
Some(path)
}
pub fn workspace_file_or_skip_at(test: &str, manifest_dir: &Path, rel: &str) -> Option<String> {
let path = workspace_path_or_skip_at(test, manifest_dir, rel)?;
let text = std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("{test}: in tree, {} must be readable: {e}", path.display()));
Some(text)
}
#[macro_export]
macro_rules! workspace_path_or_skip {
($test:expr, $rel:expr $(,)?) => {
$crate::tree::workspace_path_or_skip_at(
$test,
::std::path::Path::new(env!("CARGO_MANIFEST_DIR")),
$rel,
)
};
}
#[macro_export]
macro_rules! workspace_file_or_skip {
($test:expr, $rel:expr $(,)?) => {
$crate::tree::workspace_file_or_skip_at(
$test,
::std::path::Path::new(env!("CARGO_MANIFEST_DIR")),
$rel,
)
};
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
fn fixture(
manifest: Option<&str>,
crate_rel: &str,
with_contracts: bool,
) -> (tempfile::TempDir, PathBuf) {
let tmp = tempfile::tempdir().unwrap();
if let Some(text) = manifest {
fs::write(tmp.path().join("Cargo.toml"), text).unwrap();
}
let krate = tmp.path().join(crate_rel);
fs::create_dir_all(&krate).unwrap();
fs::write(krate.join("Cargo.toml"), "[package]\nname = \"foo\"\n").unwrap();
if with_contracts {
fs::create_dir_all(tmp.path().join("contracts")).unwrap();
fs::write(tmp.path().join("contracts/x.yaml"), "k: v\n").unwrap();
}
(tmp, krate)
}
const WS: &str = "[workspace]\nmembers = [\"crates/*\"]\n";
fn outcome(manifest: Option<&str>, crate_rel: &str, with_contracts: bool) -> &'static str {
let (_tmp, krate) = fixture(manifest, crate_rel, with_contracts);
match std::panic::catch_unwind(|| {
workspace_file_or_skip_at("case", &krate, "contracts/x.yaml")
}) {
Ok(Some(_)) => "run",
Ok(None) => "skip",
Err(_) => "FAIL",
}
}
#[test]
fn case_table() {
let rows: &[(&str, Option<&str>, &str, bool, &str)] = &[
(
"in tree, contracts/ present",
Some(WS),
"crates/foo",
true,
"run",
),
(
"in tree, contracts/ absent (a broken checkout)",
Some(WS),
"crates/foo",
false,
"FAIL",
),
(
"in tree, [workspace] with a trailing comment",
Some("[workspace] # root\n"),
"crates/foo",
false,
"FAIL",
),
(
"tarball: no manifest two levels up (the registry)",
None,
"src/foo-1.2.3",
true,
"skip",
),
(
"tarball: a parent manifest without [workspace]",
Some("[package]\nname = \"p\"\n"),
"crates/foo",
true,
"skip",
),
(
"tarball gate: a generated [workspace], crate under pkgs/",
Some(WS),
"pkgs/foo-1.2.3",
true,
"skip",
),
(
"[workspace] only in a comment",
Some("# [workspace]\n[package]\n"),
"crates/foo",
true,
"skip",
),
];
let mut bad = Vec::new();
for (name, manifest, crate_rel, with_contracts, want) in rows {
let got = outcome(*manifest, crate_rel, *with_contracts);
if got != *want {
bad.push(format!("{name}: want {want}, got {got}"));
}
}
assert!(
bad.is_empty(),
"case table rows failed:\n{}",
bad.join("\n")
);
}
#[test]
fn a_same_named_crates_dir_that_is_not_this_crate_is_out_of_tree() {
let (tmp, krate) = fixture(Some(WS), "vendor/foo", true);
fs::create_dir_all(tmp.path().join("crates/foo")).unwrap();
assert_eq!(
workspace_file_or_skip_at("decoy", &krate, "contracts/x.yaml"),
None,
"crates/foo exists but is not {}: out of tree",
krate.display()
);
}
#[test]
fn the_macro_reads_the_callers_manifest_dir() {
if let Some(text) = crate::workspace_file_or_skip!(
"the_macro_reads_the_callers_manifest_dir",
"crates/aprender-contracts/Cargo.toml"
) {
assert!(text.contains("name = \"aprender-contracts\""));
}
}
#[test]
fn the_macros_accept_the_trailing_comma_rustfmt_adds() {
let path = crate::workspace_path_or_skip!(
"the_macros_accept_the_trailing_comma_rustfmt_adds",
"crates/aprender-contracts/Cargo.toml",
);
let text = crate::workspace_file_or_skip!(
"the_macros_accept_the_trailing_comma_rustfmt_adds",
"crates/aprender-contracts/Cargo.toml",
);
assert_eq!(path.is_some(), text.is_some());
}
}