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
62pub(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}