Skip to main content

forge/profile/
targets.rs

1use std::fmt;
2
3use wowlab_fs::{
4    directory::{self, EntryKind},
5    file,
6    path::Path,
7};
8
9#[derive(Clone, Debug, Eq, PartialEq)]
10pub(super) enum TargetKind {
11    Binary,
12    Bench,
13}
14
15#[derive(Clone, Debug)]
16pub(super) struct ProfileTarget {
17    pub name: String,
18    pub package: String,
19    pub bin_name: String,
20    pub kind: TargetKind,
21    pub features: Vec<String>,
22    pub default_args: Vec<String>,
23}
24
25impl fmt::Display for ProfileTarget {
26    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27        let kind = match self.kind {
28            TargetKind::Binary => "bin",
29            TargetKind::Bench => "bench",
30        };
31
32        write!(f, "{} ({})", self.name, kind)
33    }
34}
35
36pub(super) fn discover_engine_specs(crates_dir: &Path) -> Vec<String> {
37    let manifests_dir = crates_dir.join("engine-content").join("manifests");
38    let Ok(entries) = directory::entries(&manifests_dir) else {
39        return Vec::new();
40    };
41
42    let mut specs: Vec<String> = entries
43        .into_iter()
44        .filter(|entry| entry.kind() == EntryKind::File)
45        .filter_map(|entry| {
46            let name = entry
47                .path()
48                .file_name()
49                .unwrap_or_default()
50                .to_string_lossy()
51                .into_owned();
52
53            name.strip_suffix(".toml").map(ToString::to_string)
54        })
55        .collect();
56
57    specs.sort_unstable();
58
59    specs
60}
61
62// #t(fn: rust_alloc_in_loop, rust_clone_in_loop) parsing Cargo.toml entries into owned ProfileTarget structs
63pub(super) fn discover(crates_dir: &Path) -> Vec<ProfileTarget> {
64    let mut targets = Vec::new();
65
66    let Ok(entries) = directory::entries(crates_dir) else {
67        return targets;
68    };
69
70    for entry in entries {
71        if entry.kind() != EntryKind::Directory {
72            continue;
73        }
74
75        let path = entry.path();
76        let cargo_toml = path.join("Cargo.toml");
77
78        if !directory::inspect(&cargo_toml)
79            .ok()
80            .flatten()
81            .is_some_and(|entry| entry.kind() == EntryKind::File)
82        {
83            continue;
84        }
85
86        let Ok(content) = file::read_text(&cargo_toml) else {
87            continue;
88        };
89
90        let Ok(doc) = content.parse::<toml::Table>() else {
91            continue;
92        };
93
94        let package_name = doc
95            .get("package")
96            .and_then(|p| p.get("name"))
97            .and_then(|n| n.as_str())
98            .unwrap_or_default()
99            .to_string();
100
101        let dir_name = path
102            .file_name()
103            .unwrap_or_default()
104            .to_string_lossy()
105            .to_string();
106
107        if let Some(bins) = doc.get("bin").and_then(|b| b.as_array()) {
108            for bin in bins {
109                let bin_name = bin
110                    .get("name")
111                    .and_then(|n| n.as_str())
112                    .unwrap_or_default()
113                    .to_string();
114
115                if bin_name.is_empty() {
116                    continue;
117                }
118
119                let (features, default_args) = engine_defaults(&dir_name, crates_dir);
120
121                targets.push(ProfileTarget {
122                    name: dir_name.clone(),
123                    package: package_name.clone(),
124                    bin_name,
125                    kind: TargetKind::Binary,
126                    features,
127                    default_args,
128                });
129            }
130        }
131
132        if let Some(benches) = doc.get("bench").and_then(|b| b.as_array()) {
133            for bench in benches {
134                let bench_name = bench
135                    .get("name")
136                    .and_then(|n| n.as_str())
137                    .unwrap_or_default()
138                    .to_string();
139
140                if bench_name.is_empty() {
141                    continue;
142                }
143
144                targets.push(ProfileTarget {
145                    name: format!("{dir_name}/{bench_name}"),
146                    package: package_name.clone(),
147                    bin_name: bench_name,
148                    kind: TargetKind::Bench,
149                    features: Vec::new(),
150                    default_args: Vec::new(),
151                });
152            }
153        }
154    }
155
156    targets.sort_by(|a, b| a.name.cmp(&b.name));
157
158    targets
159}
160
161fn engine_defaults(dir_name: &str, crates_dir: &Path) -> (Vec<String>, Vec<String>) {
162    if dir_name == "engine" {
163        let features = vec!["cli".to_string(), "jit".to_string(), "supabase".to_string()];
164        let first_spec = discover_engine_specs(crates_dir)
165            .into_iter()
166            .next()
167            .unwrap_or_else(|| "beast_mastery_hunter".to_string());
168        let args = vec![
169            "sim".to_string(),
170            "--spec".to_string(),
171            first_spec,
172            "--iterations".to_string(),
173            "5000".to_string(),
174            "--duration".to_string(),
175            "300".to_string(),
176            "--threads".to_string(),
177            "1".to_string(),
178            "--format".to_string(),
179            "json".to_string(),
180        ];
181
182        (features, args)
183    } else {
184        (Vec::new(), Vec::new())
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use googletest::prelude::*;
191    use wowlab_fs::{directory, file, temporary::Directory};
192
193    use super::{TargetKind, discover, discover_engine_specs};
194
195    #[gtest]
196    fn target_discovery_is_sorted_and_reads_declared_bins_and_benches() -> Result<()> {
197        let temporary = Directory::new().or_fail()?;
198        let crates = temporary.path().join("crates");
199        let alpha = crates.join("alpha");
200        let zeta = crates.join("zeta");
201
202        directory::ensure(&alpha).or_fail()?;
203        directory::ensure(&zeta).or_fail()?;
204        file::write_text(
205            &alpha.join("Cargo.toml"),
206            r#"
207[package]
208name = "alpha-package"
209
210[[bin]]
211name = "alpha-bin"
212
213[[bench]]
214name = "throughput"
215"#,
216        )
217        .or_fail()?;
218        file::write_text(
219            &zeta.join("Cargo.toml"),
220            r#"
221[package]
222name = "zeta-package"
223
224[[bin]]
225name = "zeta-bin"
226"#,
227        )
228        .or_fail()?;
229
230        let targets = discover(&crates);
231        let names = targets
232            .iter()
233            .map(|target| target.name.as_str())
234            .collect::<Vec<_>>();
235        let kinds = targets
236            .iter()
237            .map(|target| target.kind.clone())
238            .collect::<Vec<_>>();
239
240        verify_eq!(names, vec!["alpha", "alpha/throughput", "zeta"])?;
241
242        verify_eq!(
243            kinds,
244            vec![TargetKind::Binary, TargetKind::Bench, TargetKind::Binary,]
245        )
246    }
247
248    #[gtest]
249    fn engine_spec_discovery_keeps_only_sorted_toml_files() -> Result<()> {
250        let temporary = Directory::new().or_fail()?;
251        let manifests = temporary.path().join("crates/engine-content/manifests");
252
253        directory::ensure(&manifests).or_fail()?;
254        file::write_text(&manifests.join("zeta.toml"), "").or_fail()?;
255        file::write_text(&manifests.join("alpha.toml"), "").or_fail()?;
256        file::write_text(&manifests.join("README.md"), "").or_fail()?;
257        directory::ensure(&manifests.join("directory.toml")).or_fail()?;
258
259        verify_that!(
260            discover_engine_specs(&temporary.path().join("crates")),
261            elements_are!["alpha", "zeta"]
262        )
263    }
264}