use anyhow::{bail, Context, Result};
use std::env;
use std::fs;
use std::path::{Path, PathBuf};
const LOWERING_VERSION: &str = env!("CARGO_PKG_VERSION");
fn wrapper_fingerprint() -> u64 {
use std::sync::OnceLock;
static FP: OnceLock<u64> = OnceLock::new();
*FP.get_or_init(|| {
let Ok(exe) = env::current_exe() else {
return 0;
};
let Ok(meta) = fs::metadata(&exe) else {
return 0;
};
let mtime = meta
.modified()
.ok()
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_nanos() as u64)
.unwrap_or(0);
meta.len() ^ mtime
})
}
pub fn source_cache_key(source: &str) -> u64 {
const FNV_OFFSET: u64 = 0xcbf29ce484222325;
const FNV_PRIME: u64 = 0x100000001b3;
let mut hash = FNV_OFFSET;
for byte in LOWERING_VERSION
.bytes()
.chain(wrapper_fingerprint().to_le_bytes())
.chain(source.bytes())
{
hash ^= byte as u64;
hash = hash.wrapping_mul(FNV_PRIME);
}
hash
}
pub struct Prepared {
pub lowered_root: PathBuf,
pub remap_flag: String,
}
pub fn collect_crate_callees(src_dir: &Path) -> Vec<(String, Vec<String>)> {
use std::collections::{HashMap, HashSet};
let mut sigs: HashMap<String, Vec<String>> = HashMap::new();
let mut ambiguous: HashSet<String> = HashSet::new();
let mut visited: HashSet<PathBuf> = HashSet::new();
collect_crate_callees_recursive(src_dir, &mut sigs, &mut ambiguous, &mut visited);
let mut out: Vec<(String, Vec<String>)> = sigs.into_iter().collect();
out.sort_by(|a, b| a.0.cmp(&b.0));
out
}
fn collect_crate_callees_recursive(
dir: &Path,
sigs: &mut std::collections::HashMap<String, Vec<String>>,
ambiguous: &mut std::collections::HashSet<String>,
visited: &mut std::collections::HashSet<PathBuf>,
) {
if !dir.is_dir() {
return;
}
let Ok(read) = fs::read_dir(dir) else {
return;
};
for entry in read.flatten() {
let path = entry.path();
let canonical = path.canonicalize().unwrap_or_else(|_| path.clone());
if !visited.insert(canonical) {
continue;
}
if path.is_dir() {
collect_crate_callees_recursive(&path, sigs, ambiguous, visited);
} else if path.extension().and_then(|e| e.to_str()) == Some("rs") {
if let Ok(source) = fs::read_to_string(&path) {
let file = syn::parse_file(&source).ok().or_else(|| {
trust_lower::lower(&source)
.ok()
.and_then(|lo| syn::parse_file(&lo.source).ok())
});
if let Some(file) = file {
walk_items_for_sigs(&file.items, sigs, ambiguous);
}
}
}
}
}
fn walk_items_for_sigs(
items: &[syn::Item],
sigs: &mut std::collections::HashMap<String, Vec<String>>,
ambiguous: &mut std::collections::HashSet<String>,
) {
for item in items {
match item {
syn::Item::Fn(f) => record_fn_sig(&f.sig, sigs, ambiguous),
syn::Item::Mod(m) => {
if let Some((_, inner)) = &m.content {
walk_items_for_sigs(inner, sigs, ambiguous);
}
}
_ => {}
}
}
}
fn record_fn_sig(
sig: &syn::Signature,
sigs: &mut std::collections::HashMap<String, Vec<String>>,
ambiguous: &mut std::collections::HashSet<String>,
) {
let name = sig.ident.to_string();
if ambiguous.contains(&name) {
return;
}
let mut params: Vec<String> = Vec::new();
for input in &sig.inputs {
match input {
syn::FnArg::Receiver(_) => {} syn::FnArg::Typed(pat_type) => match &*pat_type.pat {
syn::Pat::Ident(pi) => params.push(pi.ident.to_string()),
_ => {
sigs.remove(&name);
ambiguous.insert(name);
return;
}
},
}
}
match sigs.get(&name) {
Some(existing) if existing != ¶ms => {
sigs.remove(&name);
ambiguous.insert(name);
}
Some(_) => {}
None => {
sigs.insert(name, params);
}
}
}
pub fn find_input_rs(args: &[String]) -> Option<usize> {
args.iter().enumerate().find_map(|(i, a)| {
if a == "-" {
return None;
}
if a.ends_with(".rs") && !a.starts_with('-') {
Some(i)
} else {
None
}
})
}
pub fn crate_is_force_strict() -> bool {
force_strict_for(
env::var("TRUST_STRICT_PACKAGES").ok().as_deref(),
env::var("CARGO_PKG_NAME").ok().as_deref(),
)
}
fn force_strict_for(pkgs: Option<&str>, name: Option<&str>) -> bool {
let (Some(pkgs), Some(name)) = (pkgs, name) else {
return false;
};
let name = name.trim();
!name.is_empty() && pkgs.split(',').any(|p| p.trim() == name)
}
fn should_lower(source: &str) -> bool {
trust_lower::is_strict_source(source) || crate_is_force_strict()
}
pub fn prepare_strict_input(input_path: &Path) -> Result<Option<Prepared>> {
let source = match fs::read_to_string(input_path) {
Ok(s) => s,
Err(_) => return Ok(None),
};
if !should_lower(&source) {
return Ok(None);
}
let force_strict = crate_is_force_strict();
let file_name = input_path
.file_name()
.context("input path has no file name")?;
let cache_key = source_cache_key(&source);
let cache_root = env::temp_dir().join("trust-cache");
let cache_dir = cache_root.join(format!("{cache_key:016x}"));
let cached_file = cache_dir.join(file_name);
if !cache_dir.exists() {
let staging = cache_root.join(format!(".staging-{cache_key:016x}-{}", std::process::id()));
let _ = fs::remove_dir_all(&staging);
let result = (|| -> Result<()> {
let src_dir = input_path
.parent()
.filter(|p| !p.as_os_str().is_empty())
.map(Path::to_path_buf)
.unwrap_or_else(|| PathBuf::from("."));
let crate_extras = collect_crate_callees(&src_dir);
let dep_extras = trust_lower::sig_index::load_from_env();
let extras = trust_lower::sig_index::merge(&[crate_extras, dep_extras]);
let mut visited = std::collections::HashSet::new();
mirror_module_tree_with_extras(&src_dir, &staging, &mut visited, &extras)
.with_context(|| format!("mirroring src tree from {}", src_dir.display()))?;
if !staging.join(file_name).exists() {
let out =
trust_lower::lower_with_extra_callees_forced(&source, &extras, force_strict)
.with_context(|| format!("lowering {}", input_path.display()))?;
emit_diagnostics(&out, &source, input_path)?;
fs::create_dir_all(&staging)?;
fs::write(staging.join(file_name), &out.source)?;
}
Ok(())
})();
if let Err(e) = result {
let _ = fs::remove_dir_all(&staging);
return Err(e);
}
if fs::rename(&staging, &cache_dir).is_err() {
let _ = fs::remove_dir_all(&staging);
if !cache_dir.exists() {
bail!(
"could not publish lowering cache at {}",
cache_dir.display()
);
}
}
}
let parent = input_path
.parent()
.filter(|p| !p.as_os_str().is_empty())
.map(Path::to_path_buf)
.unwrap_or_else(|| PathBuf::from("."));
Ok(Some(Prepared {
lowered_root: cached_file,
remap_flag: format!(
"--remap-path-prefix={}={}",
cache_dir.display(),
parent.display()
),
}))
}
fn emit_diagnostics(
out: &trust_lower::LowerOutput,
original_source: &str,
path: &Path,
) -> Result<()> {
emit_diagnostics_to(out, original_source, path, &mut std::io::stderr())
}
fn message_format_is_json() -> bool {
env::var("TRUST_MESSAGE_FORMAT").is_ok_and(|v| v == "json")
}
fn emit_diagnostics_to(
out: &trust_lower::LowerOutput,
original_source: &str,
path: &Path,
writer: &mut impl std::io::Write,
) -> Result<()> {
let mut diagnostics = out.diagnostics.clone();
if out.strict_mode {
let file: syn::File = syn::parse_str(&out.lint_source)
.with_context(|| format!("re-parsing lowered source from {}", path.display()))?;
diagnostics.extend(trust_lints::lint_strict(&file, original_source, true).diagnostics);
}
if message_format_is_json() {
let name = path.display().to_string();
let doc = trust_diag::to_json(
&diagnostics,
trust_diag::NamedSource {
name: &name,
text: original_source,
},
);
write!(writer, "{doc}")?;
if !doc.ends_with('\n') {
writeln!(writer)?;
}
} else {
for diag in &diagnostics {
writeln!(
writer,
"[{}] {}: {}",
diag.rule,
if diag.is_error() { "error" } else { "warning" },
diag.message
)?;
}
}
if diagnostics.iter().any(|d| d.is_error()) {
bail!("trust check failed on {}", path.display());
}
Ok(())
}
pub fn collect_test_only_files(src_dir: &Path) -> std::collections::HashSet<PathBuf> {
use std::collections::HashSet;
let mut all_files: Vec<PathBuf> = Vec::new();
collect_rs_files(src_dir, &mut all_files);
let mut decls: Vec<(PathBuf, String, bool)> = Vec::new();
for file in &all_files {
let Ok(source) = fs::read_to_string(file) else {
continue;
};
let Ok(tokens) = source.parse::<proc_macro2::TokenStream>() else {
continue;
};
for (name, is_test) in file_mod_declarations(&tokens) {
decls.push((file.clone(), name, is_test));
}
}
let resolve = |declaring: &Path, name: &str| -> Option<PathBuf> {
let dir = declaring.parent()?;
let flat = dir.join(format!("{name}.rs"));
if flat.is_file() {
return flat.canonicalize().ok();
}
let nested = dir.join(name).join("mod.rs");
if nested.is_file() {
return nested.canonicalize().ok();
}
None
};
let mut test_only: HashSet<PathBuf> = HashSet::new();
loop {
let mut grew = false;
for (declaring, name, is_test) in &decls {
let from_test_file = declaring
.canonicalize()
.map(|c| test_only.contains(&c))
.unwrap_or(false);
if !is_test && !from_test_file {
continue;
}
if let Some(target) = resolve(declaring, name) {
grew |= test_only.insert(target);
}
}
if !grew {
break;
}
}
test_only
}
fn collect_rs_files(dir: &Path, out: &mut Vec<PathBuf>) {
let Ok(read) = fs::read_dir(dir) else {
return;
};
for entry in read.flatten() {
let path = entry.path();
if path.is_dir() {
collect_rs_files(&path, out);
} else if path.extension().and_then(|e| e.to_str()) == Some("rs") {
out.push(path);
}
}
}
fn cfg_args_positively_test(tokens: &proc_macro2::TokenStream) -> bool {
use proc_macro2::{Delimiter, TokenTree};
let trees: Vec<TokenTree> = tokens.clone().into_iter().collect();
let mut i = 0;
while i < trees.len() {
match &trees[i] {
TokenTree::Ident(id) if *id == "not" => {
if matches!(trees.get(i + 1), Some(TokenTree::Group(g)) if g.delimiter() == Delimiter::Parenthesis)
{
i += 2;
continue;
}
i += 1;
}
TokenTree::Ident(id) if *id == "any" || *id == "all" => {
if let Some(TokenTree::Group(g)) = trees.get(i + 1) {
if g.delimiter() == Delimiter::Parenthesis
&& cfg_args_positively_test(&g.stream())
{
return true;
}
i += 2;
continue;
}
i += 1;
}
TokenTree::Ident(id) if *id == "test" => {
let followed_by_eq = matches!(
trees.get(i + 1),
Some(TokenTree::Punct(p)) if p.as_char() == '='
);
if !followed_by_eq {
return true;
}
i += 1;
}
_ => i += 1,
}
}
false
}
fn file_mod_declarations(tokens: &proc_macro2::TokenStream) -> Vec<(String, bool)> {
use proc_macro2::{Delimiter, TokenTree};
let trees: Vec<TokenTree> = tokens.clone().into_iter().collect();
let mut out = Vec::new();
let mut i = 0;
let mut pending_cfg_test = false;
while i < trees.len() {
match &trees[i] {
TokenTree::Punct(p) if p.as_char() == '#' => {
if let Some(TokenTree::Group(g)) = trees.get(i + 1) {
if g.delimiter() == Delimiter::Bracket {
let inner: Vec<TokenTree> = g.stream().into_iter().collect();
if let [TokenTree::Ident(name), TokenTree::Group(args)] = inner.as_slice() {
if *name == "cfg" {
pending_cfg_test |= cfg_args_positively_test(&args.stream());
}
}
i += 2;
continue;
}
}
i += 1;
}
TokenTree::Ident(id) if *id == "pub" => {
i += 1;
if let Some(TokenTree::Group(g)) = trees.get(i) {
if g.delimiter() == Delimiter::Parenthesis {
i += 1;
}
}
}
TokenTree::Ident(id) if *id == "mod" => {
if let (Some(TokenTree::Ident(name)), Some(TokenTree::Punct(semi))) =
(trees.get(i + 1), trees.get(i + 2))
{
if semi.as_char() == ';' {
out.push((name.to_string(), pending_cfg_test));
}
}
pending_cfg_test = false;
i += 1;
}
_ => {
pending_cfg_test = false;
i += 1;
}
}
}
out
}
pub fn mirror_module_tree(
src_dir: &Path,
dest_dir: &Path,
already_done: &mut std::collections::HashSet<PathBuf>,
) -> Result<()> {
mirror_module_tree_with_extras(src_dir, dest_dir, already_done, &[])
}
pub fn mirror_module_tree_with_extras(
src_dir: &Path,
dest_dir: &Path,
already_done: &mut std::collections::HashSet<PathBuf>,
extras: &[(String, Vec<String>)],
) -> Result<()> {
let test_only = if crate_is_force_strict() {
collect_test_only_files(src_dir)
} else {
std::collections::HashSet::new()
};
mirror_inner(src_dir, dest_dir, already_done, extras, &test_only)
}
fn mirror_inner(
src_dir: &Path,
dest_dir: &Path,
already_done: &mut std::collections::HashSet<PathBuf>,
extras: &[(String, Vec<String>)],
test_only: &std::collections::HashSet<PathBuf>,
) -> Result<()> {
if !src_dir.is_dir() {
return Ok(());
}
fs::create_dir_all(dest_dir).with_context(|| format!("creating {}", dest_dir.display()))?;
for entry in
fs::read_dir(src_dir).with_context(|| format!("reading dir {}", src_dir.display()))?
{
let entry = entry?;
let path = entry.path();
let dest = dest_dir.join(entry.file_name());
let canonical = path.canonicalize().unwrap_or_else(|_| path.clone());
let is_test_only = test_only.contains(&canonical);
if !already_done.insert(canonical) {
continue;
}
if path.is_dir() {
mirror_inner(&path, &dest, already_done, extras, test_only)?;
} else if path.extension().and_then(|e| e.to_str()) == Some("rs") {
let source =
fs::read_to_string(&path).with_context(|| format!("reading {}", path.display()))?;
let lower_this = trust_lower::is_strict_source(&source)
|| (crate_is_force_strict() && !is_test_only);
if lower_this {
let out = trust_lower::lower_with_extra_callees_forced(
&source,
extras,
crate_is_force_strict(),
)
.with_context(|| format!("lowering {}", path.display()))?;
emit_diagnostics(&out, &source, &path)?;
let rewritten = lower_doctests_in_source(&out.source);
let tmp = dest_dir.join(format!(
".{}.{}.tmp",
entry.file_name().to_string_lossy(),
std::process::id()
));
fs::write(&tmp, &rewritten)?;
fs::rename(&tmp, &dest)?;
} else {
fs::copy(&path, &dest).with_context(|| format!("copying {}", path.display()))?;
}
} else {
let _ = fs::copy(&path, &dest);
}
}
Ok(())
}
pub fn lower_doctests_in_source(source: &str) -> String {
let mut out = String::with_capacity(source.len());
let lines: Vec<&str> = source.lines().collect();
let mut i = 0;
while i < lines.len() {
let (Some(prefix), Some(_)) = (doc_prefix(lines[i]), doc_body(lines[i])) else {
out.push_str(lines[i]);
out.push('\n');
i += 1;
continue;
};
let block_start = i;
while i < lines.len() && doc_prefix(lines[i]) == Some(prefix) {
i += 1;
}
let block_end = i;
let block = rewrite_doc_block(&lines[block_start..block_end], prefix);
out.push_str(&block);
}
out
}
fn doc_prefix(line: &str) -> Option<&'static str> {
let trimmed = line.trim_start();
if trimmed.starts_with("///") {
Some("///")
} else if trimmed.starts_with("//!") {
Some("//!")
} else {
None
}
}
fn doc_body(line: &str) -> Option<&str> {
let trimmed = line.trim_start();
let body = trimmed
.strip_prefix("///")
.or_else(|| trimmed.strip_prefix("//!"))?;
Some(body.strip_prefix(' ').unwrap_or(body))
}
fn rewrite_doc_block(lines: &[&str], prefix: &str) -> String {
let first = lines[0];
let indent_len = first.len() - first.trim_start().len();
let indent = &first[..indent_len];
let mut out = String::new();
let mut in_block = false;
let mut is_test_block = false;
let mut code_buf = String::new();
let mut block_indent_after_prefix = String::new();
for line in lines {
let body = doc_body(line).unwrap_or("");
let body_trim = body.trim_start();
if body_trim.starts_with("```") {
if !in_block {
let info = body_trim.trim_start_matches('`').trim();
is_test_block = info.is_empty()
|| info == "rust"
|| info.starts_with("rust,")
|| info.starts_with("rust ");
in_block = true;
code_buf.clear();
block_indent_after_prefix.clear();
if let Some(stripped) = line.trim_start().strip_prefix(prefix) {
let after = stripped;
let extra_indent_len = after.len() - after.trim_start().len();
block_indent_after_prefix = after[..extra_indent_len].to_string();
}
out.push_str(line);
out.push('\n');
continue;
}
let lowered = if is_test_block {
try_lower_doctest(&code_buf).unwrap_or_else(|| code_buf.clone())
} else {
code_buf.clone()
};
for code_line in lowered.lines() {
out.push_str(indent);
out.push_str(prefix);
if !code_line.is_empty() {
if block_indent_after_prefix.is_empty() {
out.push(' ');
} else {
out.push_str(&block_indent_after_prefix);
}
}
out.push_str(code_line);
out.push('\n');
}
out.push_str(line);
out.push('\n');
in_block = false;
code_buf.clear();
continue;
}
if in_block {
code_buf.push_str(body);
code_buf.push('\n');
} else {
out.push_str(line);
out.push('\n');
}
}
if in_block {
for code_line in code_buf.lines() {
out.push_str(indent);
out.push_str(prefix);
out.push(' ');
out.push_str(code_line);
out.push('\n');
}
}
out
}
fn try_lower_doctest(snippet: &str) -> Option<String> {
if let Ok(out) = trust_lower::lower(snippet) {
if !out.diagnostics.iter().any(|d| d.is_error()) {
return Some(strip_hidden_doctest_prefix(out.source));
}
}
let wrapped = format!("fn __trust_doctest() {{\n{snippet}\n}}\n");
let out = trust_lower::lower(&wrapped).ok()?;
if out.diagnostics.iter().any(|d| d.is_error()) {
return None;
}
let unwrapped = unwrap_doctest_fn(&out.source)?;
Some(unwrapped)
}
fn unwrap_doctest_fn(source: &str) -> Option<String> {
let start = source.find("fn __trust_doctest()")?;
let open = source[start..].find('{')? + start;
let bytes = source.as_bytes();
let mut depth = 0i32;
let mut close = None;
for (i, &b) in bytes.iter().enumerate().skip(open) {
match b {
b'{' => depth += 1,
b'}' => {
depth -= 1;
if depth == 0 {
close = Some(i);
break;
}
}
_ => {}
}
}
let close = close?;
let body = &source[open + 1..close];
let mut lines: Vec<String> = body.lines().map(|l| l.to_string()).collect();
while lines.first().is_some_and(|l| l.trim().is_empty()) {
lines.remove(0);
}
while lines.last().is_some_and(|l| l.trim().is_empty()) {
lines.pop();
}
let dedent = lines
.iter()
.filter(|l| !l.trim().is_empty())
.map(|l| l.len() - l.trim_start().len())
.min()
.unwrap_or(0);
let out: String = lines
.iter()
.map(|l| {
if l.len() >= dedent {
format!("{}\n", &l[dedent..])
} else {
"\n".to_string()
}
})
.collect();
Some(out)
}
fn strip_hidden_doctest_prefix(s: String) -> String {
s
}
#[cfg(test)]
mod tests {
use super::*;
static MESSAGE_FORMAT_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
struct MessageFormatGuard<'a> {
prev: Option<String>,
_lock: std::sync::MutexGuard<'a, ()>,
}
impl MessageFormatGuard<'_> {
fn set(value: Option<&str>) -> Self {
let lock = MESSAGE_FORMAT_LOCK
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let prev = env::var("TRUST_MESSAGE_FORMAT").ok();
match value {
Some(v) => env::set_var("TRUST_MESSAGE_FORMAT", v),
None => env::remove_var("TRUST_MESSAGE_FORMAT"),
}
MessageFormatGuard { prev, _lock: lock }
}
}
impl Drop for MessageFormatGuard<'_> {
fn drop(&mut self) {
match &self.prev {
Some(prev) => env::set_var("TRUST_MESSAGE_FORMAT", prev),
None => env::remove_var("TRUST_MESSAGE_FORMAT"),
}
}
}
#[test]
fn json_message_format_emits_parseable_document() {
let _guard = MessageFormatGuard::set(Some("json"));
let source =
"#![strict]\nfn main() { let v: Option<i32> = Some(1); let _ = v.unwrap(); }\n";
let out = trust_lower::lower(source).expect("lowering strict source");
let mut buf: Vec<u8> = Vec::new();
let result = emit_diagnostics_to(&out, source, Path::new("src/main.rs"), &mut buf);
assert!(result.is_err(), "R0001 is an error — must still bail");
let text = String::from_utf8(buf).expect("utf8 output");
let doc: serde_json::Value =
serde_json::from_str(text.trim()).expect("output must be valid JSON");
assert_eq!(doc["file"], "src/main.rs");
let rules: Vec<&str> = doc["diagnostics"]
.as_array()
.expect("diagnostics array")
.iter()
.filter_map(|d| d["rule"].as_str())
.collect();
assert!(rules.contains(&"R0001"), "expected R0001 in {rules:?}");
}
#[test]
fn default_message_format_is_human_lines() {
let _guard = MessageFormatGuard::set(None);
let source =
"#![strict]\nfn main() { let v: Option<i32> = Some(1); let _ = v.unwrap(); }\n";
let out = trust_lower::lower(source).expect("lowering strict source");
let mut buf: Vec<u8> = Vec::new();
let result = emit_diagnostics_to(&out, source, Path::new("src/main.rs"), &mut buf);
assert!(result.is_err());
let text = String::from_utf8(buf).expect("utf8 output");
assert!(
text.contains("[R0001] error:"),
"expected human line, got: {text}"
);
}
#[test]
fn cfg_test_mod_files_are_detected_transitively() {
let base = std::env::temp_dir().join(format!("trust-rt88-{}", std::process::id()));
let src = base.join("src");
let _ = fs::remove_dir_all(&base);
fs::create_dir_all(&src).unwrap();
fs::write(
src.join("main.rs"),
"mod shipping;\n#[cfg(test)]\nmod tests;\nfn main() {}\n",
)
.unwrap();
fs::write(src.join("shipping.rs"), "pub fn ship() {}\n").unwrap();
fs::write(src.join("tests.rs"), "mod helpers;\nfn t() {}\n").unwrap();
fs::write(src.join("helpers.rs"), "pub fn helper() {}\n").unwrap();
let test_only = collect_test_only_files(&src);
let has = |name: &str| {
test_only
.iter()
.any(|p| p.file_name().and_then(|f| f.to_str()) == Some(name))
};
assert!(has("tests.rs"), "directly cfg(test)-declared file");
assert!(has("helpers.rs"), "transitively reached through tests.rs");
assert!(!has("shipping.rs"), "normal mod stays enforced");
assert!(!has("main.rs"), "the crate root is never test-only");
let _ = fs::remove_dir_all(&base);
}
#[test]
fn negated_test_cfgs_are_not_test_only() {
let base = std::env::temp_dir().join(format!("trust-pr1-{}", std::process::id()));
let src = base.join("src");
let _ = fs::remove_dir_all(&base);
fs::create_dir_all(&src).unwrap();
fs::write(
src.join("main.rs"),
"#[cfg(not(test))]\nmod prod;\n\
#[cfg(all(unix, not(test)))]\nmod prod_unix;\n\
#[cfg(all(unix, test))]\nmod unix_tests;\n\
#[cfg(test)]\nmod tests;\n\
#[cfg(feature = \"test\")]\nmod feature_named_test;\n\
fn main() {}\n",
)
.unwrap();
for name in [
"prod.rs",
"prod_unix.rs",
"unix_tests.rs",
"tests.rs",
"feature_named_test.rs",
] {
fs::write(src.join(name), "pub fn x() {}\n").unwrap();
}
let test_only = collect_test_only_files(&src);
let has = |name: &str| {
test_only
.iter()
.any(|p| p.file_name().and_then(|f| f.to_str()) == Some(name))
};
assert!(!has("prod.rs"), "cfg(not(test)) is a production module");
assert!(!has("prod_unix.rs"), "all(unix, not(test)) is production");
assert!(has("unix_tests.rs"), "all(unix, test) is test-only");
assert!(has("tests.rs"), "plain cfg(test) is test-only");
assert!(
!has("feature_named_test.rs"),
"feature = \"test\" is a feature gate, not the test predicate"
);
let _ = fs::remove_dir_all(&base);
}
#[test]
fn force_strict_is_scoped_by_package_name() {
assert!(force_strict_for(Some("my-app"), Some("my-app")));
assert!(!force_strict_for(Some("my-app"), Some("serde")));
assert!(force_strict_for(Some("a, b ,c"), Some("b")));
assert!(!force_strict_for(None, Some("my-app")));
assert!(!force_strict_for(Some("my-app"), None));
assert!(!force_strict_for(Some("a,"), Some("")));
}
#[test]
fn mirror_copies_rather_than_hardlinks_source() {
let base = std::env::temp_dir().join(format!("trust-rt75-{}", std::process::id()));
let src = base.join("src");
let dest = base.join("cache");
let _ = fs::remove_dir_all(&base);
fs::create_dir_all(&src).expect("create src");
let src_file = src.join("plain.rs");
fs::write(&src_file, "pub fn keep() {}\n").expect("write src");
let mut visited = std::collections::HashSet::new();
mirror_module_tree(&src, &dest, &mut visited).expect("mirror");
fs::write(dest.join("plain.rs"), "").expect("clobber cache");
let after = fs::read_to_string(&src_file).expect("read src after");
assert_eq!(
after, "pub fn keep() {}\n",
"source file was corrupted — cache shares an inode with it"
);
let _ = fs::remove_dir_all(&base);
}
}