wowlab_tidy/languages/toml/rules/cargo/
bench_debug.rs1#[cfg(test)]
2use googletest::prelude::*;
3use wowlab_fs::{
4 directory::{self, EntryKind},
5 file,
6 path::Path,
7};
8
9use super::{CARGO_WORKSPACE_REL, cargo_document, key_line, nested_table};
10use crate::{Example, TomlCtx, Violation, violation};
11
12const FULL_DEBUG_INFO: i64 = 2;
13
14#[rustfmt::skip]
15const EXAMPLES: &[Example] = &[
16 Example { label: "bench profile with line tables", code: "[workspace]\nmembers = [\"engine\"]\n\n[profile.bench]\ndebug = 1\n", pass: true },
17 Example { label: "bench profile with named debug level", code: "[workspace]\nmembers = [\"engine\"]\n\n[profile.bench]\ndebug = \"line-tables-only\"\n", pass: true },
18 Example { label: "no bench profile", code: "[workspace]\nmembers = [\"engine\"]\n", pass: false },
19 Example { label: "bench profile without debug info", code: "[workspace]\nmembers = [\"engine\"]\n\n[profile.bench]\nlto = false\n", pass: false },
20 Example { label: "bench profile with debug disabled", code: "[workspace]\nmembers = [\"engine\"]\n\n[profile.bench]\ndebug = 0\n", pass: false },
21];
22
23crate::toml_rule!(
24 toml_cargo_bench_debug,
25 "Require [profile.bench] to enable debug info when any workspace member ships benchmarks.",
26 "Profilers need symbols to attribute samples; benchmarks compiled without debug info produce unreadable hot-path profiles (M-HOTPATH).",
27 Low,
28);
29
30fn check_toml_cargo_bench_debug(ctx: &TomlCtx<'_>) -> Vec<Violation> {
31 if ctx.file.rel != CARGO_WORKSPACE_REL {
32 return Vec::new();
33 }
34
35 let Some(document) = cargo_document(ctx) else {
36 return Vec::new();
37 };
38
39 if !members_have_benches(ctx, &document) {
40 return Vec::new();
41 }
42
43 bench_profile_violations(ctx.file.rel, ctx.file.lines, &document)
44}
45
46fn bench_profile_violations(rel: &str, lines: &[&str], document: &toml::Table) -> Vec<Violation> {
47 let enables_debug = nested_table(document, &["profile", "bench"])
48 .and_then(|profile| profile.get("debug"))
49 .is_some_and(debug_enables_symbols);
50
51 if enables_debug {
52 return Vec::new();
53 }
54
55 vec![violation(
56 rel,
57 key_line(lines, "profile.bench", "debug"),
58 "members ship benchmarks but [profile.bench] does not enable debug info; set `debug = 1` so profiles resolve symbols (M-HOTPATH)",
59 )]
60}
61
62fn debug_enables_symbols(value: &toml::Value) -> bool {
64 match value {
65 toml::Value::Integer(level) => *level == 1 || *level == FULL_DEBUG_INFO,
66 toml::Value::Boolean(enabled) => *enabled,
67 toml::Value::String(level) => level == "full" || level == "line-tables-only",
68 _ => false,
69 }
70}
71
72fn members_have_benches(ctx: &TomlCtx<'_>, document: &toml::Table) -> bool {
73 let Some(members) = nested_table(document, &["workspace"])
74 .and_then(|workspace| workspace.get("members"))
75 .and_then(toml::Value::as_array)
76 else {
77 return false;
78 };
79 let patterns: Vec<&str> = members.iter().filter_map(toml::Value::as_str).collect();
80 let Some(crates_dir) = ctx.file.path.parent() else {
81 return false;
82 };
83
84 crate::infra::workspace::member_manifests(crates_dir)
85 .iter()
86 .any(|manifest| {
87 let Some(member_dir) = manifest.parent() else {
88 return false;
89 };
90
91 if member_dir.parent() != Some(crates_dir) {
92 return false;
93 }
94
95 let Some(name) = member_dir.file_name().and_then(|name| name.to_str()) else {
96 return false;
97 };
98
99 if !patterns
100 .iter()
101 .any(|pattern| glob_match::glob_match(pattern, name))
102 {
103 return false;
104 }
105
106 directory::inspect(&member_dir.join("benches"))
107 .ok()
108 .flatten()
109 .is_some_and(|entry| entry.kind() == EntryKind::Directory)
110 || declares_bench_target(manifest)
111 })
112}
113
114fn declares_bench_target(manifest: &Path) -> bool {
115 file::read_text(manifest)
116 .ok()
117 .and_then(|contents| toml::from_str::<toml::Table>(&contents).ok())
118 .is_some_and(|document| document.contains_key("bench"))
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124
125 #[gtest]
126 fn examples() -> Result<()> {
127 for example in EXAMPLES {
128 let document: toml::Table = toml::from_str(example.code).or_fail()?;
129 let lines: Vec<&str> = example.code.lines().collect();
130 let violations = bench_profile_violations("crates/Cargo.toml", &lines, &document);
131
132 verify_eq!(violations.is_empty(), example.pass)?;
133 }
134
135 Ok(())
136 }
137
138 #[gtest]
139 fn rule_skips_when_no_member_ships_benchmarks() -> Result<()> {
140 let violations = crate::test_support::check_source_toml_at(
141 "crates/Cargo.toml",
142 "[workspace]\nmembers = [\"engine\"]\n",
143 check_toml_cargo_bench_debug,
144 );
145
146 verify_true!(violations.is_empty())?;
147
148 Ok(())
149 }
150}