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) = target_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 target_defaults(dir_name: &str, crates_dir: &Path) -> (Vec<String>, Vec<String>) {
162 if dir_name == "engine" {
163 let mut features = vec!["cli".to_string(), "jit".to_string(), "supabase".to_string()];
164
165 if cfg!(feature = "rotation-optimizations") {
166 features.push("rotation-optimizations".to_string());
167 }
168
169 let first_spec = discover_engine_specs(crates_dir)
170 .into_iter()
171 .next()
172 .unwrap_or_else(|| "beast_mastery_hunter".to_string());
173 let args = vec![
174 "sim".to_string(),
175 "--spec".to_string(),
176 first_spec,
177 "--iterations".to_string(),
178 "5000".to_string(),
179 "--duration".to_string(),
180 "300".to_string(),
181 "--threads".to_string(),
182 "1".to_string(),
183 "--format".to_string(),
184 "json".to_string(),
185 ];
186
187 return (features, args);
188 }
189
190 let features = if dir_name == "forge" && cfg!(feature = "rotation-optimizations") {
191 vec!["rotation-optimizations".to_string()]
192 } else {
193 Vec::new()
194 };
195
196 (features, Vec::new())
197}
198
199#[cfg(test)]
200mod tests {
201 use googletest::prelude::*;
202 use wowlab_fs::{directory, file, temporary::Directory};
203
204 use super::{TargetKind, discover, discover_engine_specs, target_defaults};
205
206 #[gtest]
207 fn target_discovery_is_sorted_and_reads_declared_bins_and_benches() -> Result<()> {
208 let temporary = Directory::new().or_fail()?;
209 let crates = temporary.path().join("crates");
210 let alpha = crates.join("alpha");
211 let zeta = crates.join("zeta");
212
213 directory::ensure(&alpha).or_fail()?;
214 directory::ensure(&zeta).or_fail()?;
215 file::write_text(
216 &alpha.join("Cargo.toml"),
217 r#"
218[package]
219name = "alpha-package"
220
221[[bin]]
222name = "alpha-bin"
223
224[[bench]]
225name = "throughput"
226"#,
227 )
228 .or_fail()?;
229 file::write_text(
230 &zeta.join("Cargo.toml"),
231 r#"
232[package]
233name = "zeta-package"
234
235[[bin]]
236name = "zeta-bin"
237"#,
238 )
239 .or_fail()?;
240
241 let targets = discover(&crates);
242 let names = targets
243 .iter()
244 .map(|target| target.name.as_str())
245 .collect::<Vec<_>>();
246 let kinds = targets
247 .iter()
248 .map(|target| target.kind.clone())
249 .collect::<Vec<_>>();
250
251 verify_eq!(names, vec!["alpha", "alpha/throughput", "zeta"])?;
252
253 verify_eq!(
254 kinds,
255 vec![TargetKind::Binary, TargetKind::Bench, TargetKind::Binary,]
256 )
257 }
258
259 #[gtest]
260 fn engine_spec_discovery_keeps_only_sorted_toml_files() -> Result<()> {
261 let temporary = Directory::new().or_fail()?;
262 let manifests = temporary.path().join("crates/engine-content/manifests");
263
264 directory::ensure(&manifests).or_fail()?;
265 file::write_text(&manifests.join("zeta.toml"), "").or_fail()?;
266 file::write_text(&manifests.join("alpha.toml"), "").or_fail()?;
267 file::write_text(&manifests.join("README.md"), "").or_fail()?;
268 directory::ensure(&manifests.join("directory.toml")).or_fail()?;
269
270 verify_that!(
271 discover_engine_specs(&temporary.path().join("crates")),
272 elements_are!["alpha", "zeta"]
273 )
274 }
275
276 #[gtest]
277 fn forge_target_propagates_active_rotation_optimization_feature() -> Result<()> {
278 let temporary = Directory::new().or_fail()?;
279 let (features, args) = target_defaults("forge", temporary.path());
280
281 verify_that!(args, is_empty())?;
282
283 if cfg!(feature = "rotation-optimizations") {
284 verify_that!(features, elements_are!["rotation-optimizations"])
285 } else {
286 verify_that!(features, is_empty())
287 }
288 }
289}