Skip to main content

wowlab_tidy/languages/toml/rules/cargo/
bench_debug.rs

1#[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
62// #t(rust_const_fn_candidate) string equality is not callable in const fn
63fn 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}