1#[global_allocator]
4static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
5
6mod audit;
7mod bench;
8mod compare;
9mod compare_matrix;
10mod constants;
11mod encounter_debug;
12mod encounter_fixture;
13mod encounter_html;
14mod envelope;
15mod gen_rotation_schema;
16mod intent;
17mod items;
18mod manifest_ledger;
19mod paperdoll;
20mod parcel;
21mod profile;
22mod provider;
23mod render;
24mod report;
25mod run;
26mod sim;
27mod simc;
28mod spell_fixtures;
29mod talent_attribute_summary;
30mod talent_aura_summary;
31mod talent_conformance;
32mod talent_effect_summary;
33mod talent_modifier_summary;
34mod talent_target_flag_summary;
35mod talent_target_plan_summary;
36mod trace;
37mod wowlab;
38
39use clap::{Parser, Subcommand};
40use wowlab_common::{cli, output};
41
42fn fatal(args: std::fmt::Arguments<'_>) -> ! {
43 output::error(&format!("{args}"));
44 std::process::exit(1);
45}
46
47#[derive(Parser)]
48#[command(name = "forge", about = "Workspace profiler and comparison tools.")]
49struct Args {
50 #[command(subcommand)]
51 command: Command,
52
53 #[arg(long, short, global = true)]
55 quiet: bool,
56}
57
58#[derive(Subcommand)]
59enum Command {
60 Bench(bench::BenchArgs),
62 Compare(compare::CompareArgs),
64 CompareMatrix(compare_matrix::CompareMatrixArgs),
66 Envelope(envelope::EnvelopeArgs),
68 Profile(profile::ProfileArgs),
70 Audit(audit::AuditArgs),
72 Paperdoll(paperdoll::PaperdollArgs),
74 Items(items::ItemsArgs),
76 ManifestLedger(manifest_ledger::ManifestLedgerArgs),
78 Parcel(parcel::ParcelArgs),
80 GenSpellFixtures(spell_fixtures::GenSpellFixturesArgs),
82 GenRotationSchema(gen_rotation_schema::GenRotationSchemaArgs),
84 TalentConformance(talent_conformance::TalentConformanceArgs),
86}
87
88#[cfg(test)]
89mod tests {
90 use clap::Parser;
91 use googletest::{Result as GtestResult, prelude::*};
92
93 use super::{Args, Command, encounter_fixture::EncounterFixture};
94
95 #[gtest]
96 fn both_fixture_commands_accept_every_canonical_slug() -> GtestResult<()> {
97 let fixtures = [
98 ("two_target_pack", EncounterFixture::TwoTargetPack),
99 ("boss_add_wave", EncounterFixture::BossAddWave),
100 ("two_pull_dungeon", EncounterFixture::TwoPullDungeon),
101 ];
102
103 for (slug, expected) in fixtures {
104 let compare = Args::try_parse_from([
105 "forge",
106 "compare",
107 "feral_druid",
108 "--encounter-fixture",
109 slug,
110 ])
111 .or_fail()?;
112 let Command::Compare(compare) = compare.command else {
113 return fail!("compare command expected");
114 };
115
116 verify_that!(compare.encounter_fixture, eq(Some(expected)))?;
117
118 let paperdoll = Args::try_parse_from([
119 "forge",
120 "paperdoll",
121 "feral_druid",
122 "--encounter-fixture",
123 slug,
124 ])
125 .or_fail()?;
126 let Command::Paperdoll(paperdoll) = paperdoll.command else {
127 return fail!("paperdoll command expected");
128 };
129
130 verify_that!(paperdoll.encounter_fixture, eq(Some(expected)))?;
131 }
132
133 Ok(())
134 }
135
136 #[gtest]
137 fn paperdoll_rejects_an_unknown_encounter_fixture() -> GtestResult<()> {
138 let result = Args::try_parse_from([
139 "forge",
140 "paperdoll",
141 "feral_druid",
142 "--encounter-fixture",
143 "invented_pack",
144 ]);
145 let error = result.err().or_fail()?;
146
147 verify_true!(error.to_string().contains("invalid value 'invented_pack'"))?;
148
149 Ok(())
150 }
151
152 #[gtest]
153 fn envelope_accepts_browser_reproduction_inputs() -> GtestResult<()> {
154 let parsed = Args::try_parse_from([
155 "forge",
156 "envelope",
157 "/tmp/outlaw.toml",
158 "--rotation",
159 "/tmp/outlaw.json",
160 "--seed",
161 "123",
162 "--decision-limit",
163 "50",
164 "--trace",
165 ])
166 .or_fail()?;
167 let Command::Envelope(envelope) = parsed.command else {
168 return fail!("envelope command expected");
169 };
170
171 verify_that!(
172 envelope.envelope.as_path(),
173 eq(wowlab_fs::path::Path::new("/tmp/outlaw.toml"))
174 )?;
175 verify_that!(
176 envelope.rotation.as_deref(),
177 eq(Some(wowlab_fs::path::Path::new("/tmp/outlaw.json")))
178 )?;
179 verify_that!(envelope.seed, eq(123))?;
180 verify_that!(envelope.decision_limit, eq(50))?;
181 verify_true!(envelope.trace)?;
182
183 Ok(())
184 }
185}
186
187#[cfg(test)]
188mod run_parameter_cli_tests {
189 use clap::Parser;
190 use googletest::{Result as GtestResult, prelude::*};
191
192 use super::{Args, Command, run::RunParameters};
193
194 fn command_run_parameters(command: &Command) -> Option<RunParameters> {
195 match command {
196 Command::Compare(args) => RunParameters::try_from(args).ok(),
197 Command::CompareMatrix(args) => RunParameters::try_from(args).ok(),
198 Command::Bench(args) => RunParameters::try_from(args).ok(),
199 Command::Items(args) => RunParameters::try_from(args).ok(),
200 Command::Profile(args) => RunParameters::try_from(args).ok(),
201 _ => None,
202 }
203 }
204
205 #[gtest]
206 fn run_command_defaults_remain_command_specific() -> GtestResult<()> {
207 let cases = [
208 (["forge", "compare", "fire_mage"].as_slice(), 1, 300),
209 (["forge", "compare-matrix"].as_slice(), 1, 300),
210 (["forge", "bench"].as_slice(), 500_000, 300),
211 (["forge", "items"].as_slice(), 100, 60),
212 (["forge", "profile"].as_slice(), 5_000, 300),
213 ];
214
215 for (arguments, iterations, duration) in cases {
216 let parsed = Args::try_parse_from(arguments).or_fail()?;
217 let parameters = command_run_parameters(&parsed.command).or_fail()?;
218
219 verify_that!(parameters.iterations(), eq(iterations))?;
220 verify_that!(parameters.fight_duration_secs(), eq(duration))?;
221 }
222
223 Ok(())
224 }
225
226 #[gtest]
227 fn run_commands_reject_zero_values_at_the_typed_boundary() -> GtestResult<()> {
228 let zero_iterations = [
229 ["forge", "compare", "fire_mage", "--iterations", "0"].as_slice(),
230 ["forge", "compare-matrix", "--iterations", "0"].as_slice(),
231 ["forge", "bench", "--iterations", "0"].as_slice(),
232 ["forge", "items", "--iterations", "0"].as_slice(),
233 ["forge", "profile", "--iterations", "0"].as_slice(),
234 ];
235
236 for arguments in zero_iterations {
237 let parsed = Args::try_parse_from(arguments).or_fail()?;
238 let error = match &parsed.command {
239 Command::Compare(args) => RunParameters::try_from(args).err().or_fail()?,
240 Command::CompareMatrix(args) => RunParameters::try_from(args).err().or_fail()?,
241 Command::Bench(args) => RunParameters::try_from(args).err().or_fail()?,
242 Command::Items(args) => RunParameters::try_from(args).err().or_fail()?,
243 Command::Profile(args) => RunParameters::try_from(args).err().or_fail()?,
244 _ => return fail!("command has no shared run parameters"),
245 };
246
247 verify_that!(
248 error.to_string(),
249 eq("iterations must be greater than zero")
250 )?;
251 }
252
253 Ok(())
254 }
255}
256
257#[cfg(test)]
258mod summary_command_tests {
259 use clap::Parser;
260 use googletest::{Result as GtestResult, prelude::*};
261
262 use super::{Args, Command, run::RunParameters};
263
264 #[gtest]
265 fn compare_matrix_accepts_filters_and_shared_run_parameters() -> GtestResult<()> {
266 let parsed = Args::try_parse_from([
267 "forge",
268 "compare-matrix",
269 "fire_mage",
270 "unholy_death_knight",
271 "--duration",
272 "60",
273 "--iterations",
274 "20",
275 "--jobs",
276 "2",
277 "--format",
278 "json",
279 ])
280 .or_fail()?;
281 let Command::CompareMatrix(matrix) = parsed.command else {
282 return fail!("compare-matrix command expected");
283 };
284
285 verify_that!(
286 matrix
287 .specs
288 .iter()
289 .map(AsRef::as_ref)
290 .collect::<Vec<&str>>(),
291 elements_are![eq(&"fire_mage"), eq(&"unholy_death_knight")]
292 )?;
293 verify_that!(
294 &matrix,
295 matches_pattern!(crate::compare_matrix::CompareMatrixArgs {
296 duration: eq(&60),
297 iterations: eq(&20),
298 jobs: eq(&2),
299 ..
300 })
301 )?;
302 let parameters = RunParameters::try_from(&matrix).or_fail()?;
303
304 verify_that!(parameters.iterations(), eq(20))?;
305 verify_that!(parameters.fight_duration_secs(), eq(60))?;
306
307 Ok(())
308 }
309}
310
311fn main() {
312 let args = Args::parse();
313 let _composition = wowlab_engine::composition::EngineComposition::initialize()
314 .unwrap_or_else(|error| fatal(format_args!("{error}")));
315 let app = cli::boot("forge", env!("CARGO_PKG_VERSION"), args.quiet, "FORGE_ROOT");
316
317 match args.command {
318 Command::Bench(ref bench_args) => {
319 if let Err(e) = bench::run(bench_args) {
320 fatal(format_args!("{e}"));
321 }
322 }
323 Command::Compare(ref compare_args) => {
324 if let Err(e) = compare::run(compare_args) {
325 fatal(format_args!("{e:#}"));
326 }
327 }
328 Command::CompareMatrix(ref matrix_args) => {
329 if let Err(e) = compare_matrix::run(matrix_args) {
330 fatal(format_args!("{e:#}"));
331 }
332 }
333 Command::Envelope(ref envelope_args) => {
334 if let Err(e) = envelope::run(envelope_args) {
335 fatal(format_args!("{e:#}"));
336 }
337 }
338 Command::Profile(ref profile_args) => {
339 let crates_dir = app.crates_dir();
340
341 profile::run(profile_args, &crates_dir);
342 }
343 Command::Audit(ref audit_args) => {
344 if let Err(e) = audit::run(audit_args) {
345 fatal(format_args!("{e}"));
346 }
347 }
348 Command::Paperdoll(ref pd_args) => {
349 if let Err(e) = paperdoll::run(pd_args) {
350 fatal(format_args!("{e:#}"));
351 }
352 }
353 Command::Items(ref items_args) => {
354 if let Err(e) = items::run(items_args) {
355 fatal(format_args!("{e:#}"));
356 }
357 }
358 Command::ManifestLedger(ref ledger_args) => {
359 if let Err(e) = manifest_ledger::run(ledger_args) {
360 fatal(format_args!("{e:#}"));
361 }
362 }
363 Command::Parcel(ref parcel_args) => {
364 if let Err(e) = parcel::run(parcel_args) {
365 fatal(format_args!("{e:#}"));
366 }
367 }
368 Command::GenSpellFixtures(ref gen_args) => {
369 if let Err(e) = spell_fixtures::run(gen_args) {
370 fatal(format_args!("{e:#}"));
371 }
372 }
373 Command::GenRotationSchema(ref gen_args) => {
374 if let Err(e) = gen_rotation_schema::run(gen_args) {
375 fatal(format_args!("{e:#}"));
376 }
377 }
378 Command::TalentConformance(ref conformance_args) => {
379 if let Err(e) = talent_conformance::run(conformance_args) {
380 fatal(format_args!("{e:#}"));
381 }
382 }
383 }
384}