• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

adierking / unplug / 16555866747

27 Jul 2025 09:53PM UTC coverage: 77.137%. First build
16555866747

push

github

adierking
cli: Move script dump commands into debug

43 of 139 new or added lines in 2 files covered. (30.94%)

20348 of 26379 relevant lines covered (77.14%)

1083026.5 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.88
/unplug-cli/src/args.rs
1
#![allow(trivial_numeric_casts, variant_size_differences)]
2

3
use anyhow::{anyhow, Result};
4
use clap::{ArgAction, Args, Parser, Subcommand};
5
use std::path::PathBuf;
6

7
/// The minimum accepted volume level for playback.
8
const MIN_VOLUME: i32 = 0;
9
/// The maximum accepted volume level for playback.
10
const MAX_VOLUME: i32 = 100;
11

12
#[derive(Parser)]
483✔
13
#[clap(name = "Unplug", version)]
14
#[clap(about = "Chibi-Robo! Plug Into Adventure! Modding Toolkit")]
15
#[clap(help_expected = true, infer_subcommands = true)]
16
pub struct CliArgs {
17
    /// Show debug logs
18
    ///
19
    /// Use -vv in non-distribution builds to show trace logs as well
20
    #[clap(short, long, action = ArgAction::Count, global(true))]
21
    pub verbose: u8,
×
22

23
    /// Capture inferno trace data to a file (for developers)
24
    #[cfg(feature = "trace")]
25
    #[clap(long, value_name("PATH"), global(true))]
26
    pub trace: Option<PathBuf>,
27

28
    #[clap(flatten)]
29
    pub config: GlobalConfigArgs,
30

31
    #[clap(flatten)]
32
    pub context: GlobalContextArgs,
33

34
    #[clap(subcommand)]
35
    pub command: Command,
36
}
37

38
#[derive(Args)]
483✔
39
pub struct GlobalConfigArgs {
40
    /// Path to the config file to use (will be created if necessary)
41
    #[clap(long, value_name("PATH"), global(true))]
42
    pub config: Option<PathBuf>,
43

44
    /// Ignore the config file and use default settings instead
45
    #[clap(long, global(true), conflicts_with("config"))]
46
    pub no_config: bool,
×
47
}
48

49
#[derive(Args)]
483✔
50
pub struct GlobalContextArgs {
51
    /// Run the command on an ISO
52
    #[clap(long, value_name("PATH"), global(true), group("context"))]
53
    pub iso: Option<PathBuf>,
54

55
    /// Run the command on the default ISO
56
    #[clap(long, global(true), group("context"))]
57
    pub default_iso: bool,
×
58

59
    /// Use a project instead of the current one
60
    #[clap(short, long, value_name("NAME"), global(true), group("context"))]
61
    pub project: Option<String>,
62
}
63

64
#[derive(Subcommand)]
2,559✔
65
pub enum Command {
66
    /// View, edit, or extract U8 archives
67
    #[clap(subcommand)]
68
    Archive(archive::Subcommand),
69

70
    /// Export, import, or play audio resources
71
    #[clap(subcommand)]
72
    Audio(audio::Subcommand),
73

74
    /// Edit Unplug configuration options
75
    #[clap(subcommand)]
76
    Config(config::Subcommand),
77

78
    /// Run Dolphin with the current project/ISO
79
    Dolphin(dolphin::Args),
80

81
    /// Edit global metadata
82
    #[clap(subcommand)]
83
    Globals(globals::Subcommand),
84

85
    /// View, edit, or extract the game ISO
86
    #[clap(subcommand)]
87
    Iso(iso::Subcommand),
88

89
    /// List information about game assets
90
    #[clap(subcommand)]
91
    List(list::Subcommand),
92

93
    /// Edit cutscene messages
94
    #[clap(subcommand)]
95
    Messages(messages::Subcommand),
96

97
    /// Manage Unplug projects
98
    #[clap(subcommand)]
99
    Project(project::Subcommand),
100

101
    /// View, edit, or extract qp.bin
102
    ///
103
    /// This is an alias for `archive dvd:qp.bin`.
104
    #[clap(subcommand)]
105
    Qp(archive::QpSubcommand),
106

107
    /// Edit event scripts
108
    #[clap(subcommand)]
109
    Script(script::Subcommand),
110

111
    /// Edit the in-game shop
112
    #[clap(subcommand)]
113
    Shop(shop::Subcommand),
114

115
    /// Edit stage data
116
    #[clap(subcommand)]
117
    Stage(stage::Subcommand),
118

119
    #[cfg(feature = "debug")]
120
    #[clap(subcommand)]
121
    /// Debugging commands (development builds only)
122
    Debug(debug::Subcommand),
123

124
    #[cfg(test)]
125
    Test,
126
}
127

128
pub mod config {
129
    use super::*;
130

131
    #[derive(Subcommand)]
75✔
132
    pub enum Subcommand {
133
        /// Reset all configuration options to their default values
134
        Clear,
135
        /// Print the absolute path to the config file
136
        Path,
137
        /// Print the value of a setting
138
        #[clap(subcommand)]
139
        Get(GetSetting),
140
        /// Set the value of a setting
141
        #[clap(subcommand)]
142
        Set(SetSetting),
143
    }
144

145
    #[derive(Subcommand)]
9✔
146
    pub enum GetSetting {
147
        /// A path to an ISO to load by default
148
        DefaultIso,
149
        /// The path to the Dolphin executable (or macOS app bundle) to run projects with
150
        DolphinPath,
151
    }
152

153
    #[derive(Subcommand)]
18✔
154
    pub enum SetSetting {
155
        /// A path to an ISO to load by default
156
        DefaultIso {
157
            /// The new path
158
            path: Option<String>,
159
        },
160
        /// The path to the Dolphin executable (or macOS app bundle) to run projects with
161
        DolphinPath {
162
            /// The new path
163
            path: Option<String>,
164
        },
165
    }
166
}
167

168
pub mod project {
169
    use super::*;
170

171
    #[derive(Subcommand)]
228✔
172
    pub enum Subcommand {
173
        /// Show information about a project (or the current one)
174
        Info(InfoArgs),
175
        /// List defined projects
176
        List,
177
        /// Create a new project by copying the default ISO
178
        New(NewArgs),
179
        /// Delete a project's files and unregister it
180
        Wipe(WipeArgs),
181
        /// Register an existing project
182
        Add(AddArgs),
183
        /// Unregister a project without deleting any of its files
184
        #[clap(alias = "forget")]
185
        Remove(RemoveArgs),
186
        /// Open a project to be automatically used for future Unplug commands
187
        Open(OpenArgs),
188
        /// Close the currently-open project
189
        Close,
190
    }
191

192
    #[derive(Args)]
×
193
    pub struct InfoArgs {
194
        /// Name of the project to show
195
        pub name: Option<String>,
196
    }
197

198
    #[derive(Args)]
483✔
199
    pub struct AddArgs {
200
        /// Path to the project file(s)
201
        pub path: PathBuf,
×
202

203
        /// The project name (defaults to the filename)
204
        #[clap(short, long)]
205
        pub name: Option<String>,
206
    }
207

208
    #[derive(Args)]
483✔
209
    pub struct RemoveArgs {
210
        /// Name of the project to remove
211
        pub name: String,
×
212
    }
213

214
    #[derive(Args)]
483✔
215
    pub struct OpenArgs {
216
        /// Name of the project to open
217
        pub name: String,
×
218
    }
219

220
    #[derive(Args)]
483✔
221
    pub struct NewArgs {
222
        /// Name of the new project
223
        pub name: String,
×
224

225
        /// Path to the ISO to copy from
226
        #[clap(short, value_name("PATH"))]
227
        pub source: Option<PathBuf>,
228

229
        /// Path of the new ISO
230
        #[clap(short, value_name("PATH"))]
231
        pub output: Option<PathBuf>,
232

233
        /// Allow overwriting existing projects/files
234
        #[clap(short, long)]
235
        pub force: bool,
×
236

237
        /// Do not open the new project
238
        #[clap(long)]
239
        pub no_open: bool,
×
240
    }
241

242
    #[derive(Args)]
483✔
243
    pub struct WipeArgs {
244
        /// Name of the project to wipe
245
        pub name: String,
×
246

247
        /// Do not prompt for confirmation
248
        #[clap(short, long)]
249
        pub force: bool,
×
250
    }
251
}
252

253
pub mod list {
254
    use super::*;
255

256
    #[derive(Args)]
1,485✔
257
    pub struct Options {
258
        /// List file offsets and sizes
259
        #[clap(short, long)]
260
        pub long: bool,
×
261

262
        /// Sort files by name (default)
263
        #[clap(long, overrides_with_all(&["by_offset", "by_size"]))]
264
        pub by_name: bool,
×
265

266
        /// Sort files by offset
267
        #[clap(long, overrides_with_all(&["by_name", "by_size"]))]
268
        pub by_offset: bool,
×
269

270
        /// Sort files by size
271
        #[clap(long, overrides_with_all(&["by_name", "by_offset"]))]
272
        pub by_size: bool,
×
273

274
        /// Sort in reverse order
275
        #[clap(long)]
276
        pub reverse: bool,
×
277
    }
278

279
    #[derive(Args)]
2,913✔
280
    pub struct IdArgs {
281
        /// Sort by name (default)
282
        #[clap(long, overrides_with_all(&["by_id"]))]
283
        pub by_name: bool,
×
284

285
        /// Sort by ID number
286
        #[clap(long, overrides_with_all(&["by_name"]))]
287
        pub by_id: bool,
×
288

289
        /// Sort in reverse order
290
        #[clap(long)]
291
        pub reverse: bool,
×
292
    }
293

294
    #[derive(Subcommand)]
72✔
295
    pub enum Subcommand {
296
        /// List each item
297
        Items(ItemsArgs),
298
        /// List each type of equipment
299
        Equipment(EquipmentArgs),
300
        /// List each stage
301
        Stages(IdArgs),
302
        /// List each object
303
        Objects(IdArgs),
304
        /// List each music file
305
        Music(IdArgs),
306
        /// List each sound effect
307
        Sounds(IdArgs),
308
    }
309

310
    #[derive(Args)]
483✔
311
    pub struct ItemsArgs {
312
        #[clap(flatten)]
313
        pub settings: IdArgs,
314

315
        /// Include items without names
316
        #[clap(long)]
317
        pub show_unknown: bool,
×
318
    }
319

320
    #[derive(Args)]
483✔
321
    pub struct EquipmentArgs {
322
        #[clap(flatten)]
323
        pub settings: IdArgs,
324

325
        /// Include equipment without names
326
        #[clap(long)]
327
        pub show_unknown: bool,
×
328
    }
329
}
330

331
pub mod script {
332
    use super::*;
333

334
    #[derive(Subcommand)]
18✔
335
    pub enum Subcommand {
336
        /// Disassemble a single stage's script
337
        Disassemble(DisassembleArgs),
338
        /// Disassemble all scripts
339
        DisassembleAll(DisassembleAllArgs),
340
        /// Assemble a single stage's script
341
        Assemble(AssembleArgs),
342
    }
343

344
    #[derive(Args)]
483✔
345
    pub struct DisassembleArgs {
346
        /// Name of the stage to dump
347
        pub stage: String,
×
348

349
        /// Path to the output file
350
        #[clap(short, value_name("PATH"))]
351
        pub output: PathBuf,
×
352
    }
353

354
    #[derive(Args)]
483✔
355
    pub struct DisassembleAllArgs {
356
        /// Path to the output directory
357
        #[clap(short, value_name("PATH"))]
358
        pub output: PathBuf,
×
359
    }
360

361
    #[derive(Args)]
483✔
362
    pub struct AssembleArgs {
363
        /// Path to the assembly source
364
        #[clap(value_name("PATH"))]
365
        pub path: PathBuf,
×
366
    }
367
}
368

369
pub mod messages {
370
    use super::*;
371

372
    #[derive(Subcommand)]
9✔
373
    pub enum Subcommand {
374
        /// Export messages to an XML file
375
        Export(ExportArgs),
376
        /// Import messages from an XML file
377
        Import(ImportArgs),
378
    }
379

380
    #[derive(Args)]
483✔
381
    pub struct ExportArgs {
382
        /// Path to the output XML file
383
        #[clap(short, value_name("PATH"))]
384
        pub output: PathBuf,
×
385
    }
386

387
    #[derive(Args)]
483✔
388
    pub struct ImportArgs {
389
        /// Path to the input XML file
390
        pub input: PathBuf,
×
391
    }
392
}
393

394
pub mod globals {
395
    use super::*;
396

397
    #[derive(Subcommand)]
36✔
398
    pub enum Subcommand {
399
        /// Export global metadata to a JSON file
400
        Export(ExportArgs),
401
        /// Import global metadata from a JSON file
402
        Import(ImportArgs),
403
        /// Dump collision data to a text file
404
        DumpColliders(DumpCollidersArgs),
405
    }
406

407
    #[derive(Args)]
483✔
408
    pub struct ExportArgs {
409
        /// Don't output unnecessary whitespace
410
        #[clap(short, long)]
411
        pub compact: bool,
×
412

413
        /// Redirect output to a file instead of stdout
414
        #[clap(short, value_name("PATH"))]
415
        pub output: Option<PathBuf>,
416
    }
417

418
    #[derive(Args)]
483✔
419
    pub struct ImportArgs {
420
        /// Path to the input JSON file
421
        pub input: PathBuf,
×
422
    }
423

424
    #[derive(Args)]
×
425
    pub struct DumpCollidersArgs {
426
        /// Redirect output to a file instead of stdout
427
        #[clap(short, value_name("PATH"))]
428
        pub output: Option<PathBuf>,
429
    }
430
}
431

432
pub mod shop {
433
    use super::*;
434
    #[derive(Subcommand)]
18✔
435
    pub enum Subcommand {
436
        /// Export shop data to a JSON file
437
        Export(ExportArgs),
438
        /// Import shop data from a JSON file
439
        Import(ImportArgs),
440
    }
441

442
    #[derive(Args)]
483✔
443
    pub struct ExportArgs {
444
        /// Don't output unnecessary whitespace
445
        #[clap(short, long)]
446
        pub compact: bool,
×
447

448
        /// Redirect output to a file instead of stdout
449
        #[clap(short, value_name("PATH"))]
450
        pub output: Option<PathBuf>,
451
    }
452

453
    #[derive(Args)]
483✔
454
    pub struct ImportArgs {
455
        /// Path to the input JSON file
456
        pub input: PathBuf,
×
457
    }
458
}
459

460
pub mod audio {
461
    use super::*;
462

463
    #[derive(Subcommand)]
165✔
464
    pub enum Subcommand {
465
        /// Show information about an audio resource
466
        Info(InfoArgs),
467
        /// Export one or more audio resources to wav files
468
        Export(ExportArgs),
469
        /// Export an entire sample bank to a directory
470
        ExportBank(ExportBankArgs),
471
        /// Export all audio resources to a directory
472
        ExportAll(ExportAllArgs),
473
        /// Import an audio resource from an audio file
474
        Import(ImportArgs),
475
        /// Play an audio resource
476
        Play(PlayArgs),
477
    }
478

479
    #[derive(Args)]
1,449✔
480
    pub struct ExportSettings {
481
        /// If an audio file has cue points, export a .labels.txt file which defines the cues using
482
        /// Audacity's label track format
483
        #[clap(long)]
484
        pub labels: bool,
×
485
    }
486

487
    #[derive(Args)]
483✔
488
    pub struct ImportSettings {
489
        /// If an audio file has a .labels.txt file alongside it, import Audacity labels from it
490
        #[clap(long)]
491
        pub labels: bool,
×
492
    }
493

494
    #[derive(Args)]
483✔
495
    pub struct InfoArgs {
496
        /// The name or path of the audio resource
497
        pub name: String,
×
498
    }
499

500
    #[derive(Args)]
×
501
    pub struct ExportArgs {
502
        /// If extracting one audio resource, the path of the .wav file to write, otherwise the
503
        /// directory to write the audio files to
504
        #[clap(short, value_name("PATH"))]
505
        pub output: Option<PathBuf>,
506

507
        #[clap(flatten)]
508
        pub settings: ExportSettings,
509

510
        /// Names or paths of the audio resources to export
511
        #[clap(required = true)]
512
        pub names: Vec<String>,
9✔
513
    }
514

515
    #[derive(Args)]
483✔
516
    pub struct ExportBankArgs {
517
        /// The directory to write the bank's .wav files to (defaults to the bank name)
518
        #[clap(short, value_name("PATH"))]
519
        pub output: Option<PathBuf>,
520

521
        #[clap(flatten)]
522
        pub settings: ExportSettings,
523

524
        /// Name or path of the sample bank to export
525
        pub name: String,
×
526
    }
527

528
    #[derive(Args)]
483✔
529
    pub struct ExportAllArgs {
530
        /// The directory to write files to
531
        #[clap(short, value_name("PATH"))]
532
        pub output: PathBuf,
×
533

534
        #[clap(flatten)]
535
        pub settings: ExportSettings,
536
    }
537

538
    #[derive(Args)]
483✔
539
    pub struct ImportArgs {
540
        /// Name or path of the sound resource to import
541
        pub name: String,
×
542

543
        #[clap(flatten)]
544
        pub settings: ImportSettings,
545

546
        /// Path to the audio file to import (WAV, FLAC, MP3, OGG)
547
        pub path: PathBuf,
×
548
    }
549

550
    /// Clap value parser for parsing a playback volume
551
    fn parse_volume(s: &str) -> Result<f64> {
21✔
552
        let volume = s.parse::<i32>()?;
21✔
553
        if (MIN_VOLUME..=MAX_VOLUME).contains(&volume) {
21✔
554
            Ok(f64::from(volume) / 100.0)
15✔
555
        } else {
556
            Err(anyhow!("volume must be between {} and {}", MIN_VOLUME, MAX_VOLUME))
6✔
557
        }
558
    }
21✔
559

560
    #[derive(Args)]
483✔
561
    pub struct PlayArgs {
562
        /// Name or path of the audio resource to play
563
        pub name: String,
×
564

565
        /// Volume level as a percentage (0-100, default 80)
566
        #[clap(long, default_value = "80", allow_hyphen_values = true, value_parser = parse_volume)]
567
        pub volume: f64,
×
568
    }
569
}
570

571
pub mod dolphin {
572
    use super::*;
573

574
    #[derive(Args)]
483✔
575
    pub struct Args {
576
        /// Wait for Dolphin to exit and capture console output
577
        #[clap(short, long)]
578
        pub wait: bool,
×
579

580
        /// Show Dolphin's UI
581
        #[clap(long)]
582
        pub ui: bool,
×
583
    }
584
}
585

586
pub mod iso {
587
    use super::*;
588

589
    #[derive(Subcommand)]
108✔
590
    pub enum Subcommand {
591
        /// Show information about the ISO
592
        Info,
593
        /// List files in the ISO
594
        List(ListArgs),
595
        /// Extract files from the ISO
596
        Extract(ExtractArgs),
597
        /// Extract all files from the ISO
598
        ExtractAll(ExtractAllArgs),
599
        /// Replace a file in the ISO
600
        Replace(ReplaceArgs),
601
        /// Change properties of the ISO
602
        #[clap(subcommand)]
603
        Set(SetCommand),
604
    }
605

606
    #[derive(Subcommand)]
492✔
607
    pub enum SetCommand {
608
        /// The maker display name (max 63 bytes)
609
        Maker {
610
            /// The new maker name
611
            name: String,
×
612
        },
613
        /// The title display name (max 63 bytes)
614
        Name {
615
            /// The new title name
616
            name: String,
×
617
        },
618
    }
619

620
    #[derive(Args)]
×
621
    pub struct ListArgs {
622
        #[clap(flatten)]
623
        pub settings: list::Options,
624

625
        /// Paths to list (globbing is supported)
626
        pub paths: Vec<String>,
3✔
627
    }
628

629
    #[derive(Args)]
×
630
    pub struct ExtractArgs {
631
        /// If extracting one file, the path of the output file, otherwise the
632
        /// directory to extract files to
633
        #[clap(short, value_name("PATH"))]
634
        pub output: Option<PathBuf>,
635

636
        /// Paths of files to extract
637
        pub paths: Vec<String>,
3✔
638
    }
639

640
    #[derive(Args)]
×
641
    pub struct ExtractAllArgs {
642
        /// The directory to extract files to
643
        #[clap(short, value_name("PATH"))]
644
        pub output: Option<PathBuf>,
645
    }
646

647
    #[derive(Args)]
483✔
648
    pub struct ReplaceArgs {
649
        /// Path of the file in the ISO to replace
650
        #[clap(value_name("dest"))]
651
        pub dest_path: String,
×
652

653
        /// Path to the file to replace it with
654
        #[clap(value_name("src"))]
655
        pub src_path: PathBuf,
×
656
    }
657
}
658

659
pub mod archive {
660
    use super::*;
661

662
    #[derive(Subcommand)]
561✔
663
    pub enum Subcommand {
664
        /// Show information about the archive
665
        Info {
666
            /// Path to the U8 archive
667
            path: String,
×
668
        },
669
        /// List files in the archive
670
        List {
671
            /// Path to the U8 archive
672
            path: String,
×
673
            #[clap(flatten)]
674
            args: ListArgs,
675
        },
676
        /// Extract files from the archive
677
        Extract {
678
            /// Path to the U8 archive
679
            path: String,
×
680
            #[clap(flatten)]
681
            args: ExtractArgs,
682
        },
683
        /// Extract all files from the archive
684
        ExtractAll {
685
            /// Path to the U8 archive
686
            path: String,
×
687
            #[clap(flatten)]
688
            args: ExtractAllArgs,
689
        },
690
        /// Replace a file in the archive
691
        Replace {
692
            /// Path to the U8 archive
693
            path: String,
×
694
            #[clap(flatten)]
695
            args: ReplaceArgs,
696
        },
697
    }
698

699
    #[derive(Args)]
×
700
    pub struct ListArgs {
701
        #[clap(flatten)]
702
        pub settings: list::Options,
703

704
        /// Paths to list (globbing is supported)
705
        pub paths: Vec<String>,
6✔
706
    }
707

708
    #[derive(Args)]
×
709
    pub struct ExtractArgs {
710
        /// If extracting one file, the path of the output file, otherwise the
711
        /// directory to extract files to
712
        #[clap(short, value_name("PATH"))]
713
        pub output: Option<PathBuf>,
714

715
        /// Paths of files to extract
716
        pub paths: Vec<String>,
3✔
717
    }
718

719
    #[derive(Args)]
×
720
    pub struct ExtractAllArgs {
721
        /// The directory to extract files to
722
        #[clap(short, value_name("PATH"))]
723
        pub output: Option<PathBuf>,
724
    }
725

726
    #[derive(Args)]
966✔
727
    pub struct ReplaceArgs {
728
        /// Path of the file in the archive to replace
729
        #[clap(value_name("dest"))]
730
        pub dest_path: String,
×
731

732
        /// Path to the file to replace it with
733
        #[clap(value_name("src"))]
734
        pub src_path: PathBuf,
×
735
    }
736

737
    #[derive(Subcommand)]
45✔
738
    pub enum QpSubcommand {
739
        /// Show information about qp.bin
740
        Info,
741
        /// List files in qp.bin
742
        List(ListArgs),
743
        /// Extract files from qp.bin
744
        Extract(ExtractArgs),
745
        /// Extract all files from qp.bin
746
        ExtractAll(ExtractAllArgs),
747
        /// Replace a file in qp.bin
748
        Replace(ReplaceArgs),
749
    }
750
}
751

752
#[cfg(feature = "debug")]
753
pub mod debug {
754
    use super::*;
755

756
    #[derive(Subcommand)]
39✔
757
    pub enum Subcommand {
758
        /// Read and rewrite script data
759
        RebuildScripts,
760
        /// Dump the script data from a single stage
761
        DumpScript(DumpArgs),
762
        /// Dump all script data
763
        DumpAllScripts(DumpAllArgs),
764
    }
765

766
    #[derive(Args)]
966✔
767
    pub struct DumpFlags {
768
        /// Dump unknown structs
769
        #[clap(long)]
NEW
770
        pub dump_unknown: bool,
×
771

772
        /// Do not show file offsets
773
        #[clap(long)]
NEW
774
        pub no_offsets: bool,
×
775
    }
776

777
    #[derive(Args)]
483✔
778
    pub struct DumpArgs {
779
        /// Name of the stage to dump, or "globals" to dump globals
NEW
780
        pub stage: String,
×
781

782
        /// Redirect output to a file instead of stdout
783
        #[clap(short, value_name("PATH"))]
784
        pub output: Option<PathBuf>,
785

786
        #[clap(flatten)]
787
        pub flags: DumpFlags,
788
    }
789

790
    #[derive(Args)]
483✔
791
    pub struct DumpAllArgs {
792
        /// Path to the output directory
793
        #[clap(short, value_name("PATH"))]
NEW
794
        pub output: PathBuf,
×
795

796
        #[clap(flatten)]
797
        pub flags: DumpFlags,
798
    }
799
}
800

801
pub mod stage {
802
    use super::*;
803

804
    #[derive(Subcommand)]
57✔
805
    pub enum Subcommand {
806
        /// Export stage data to a JSON file
807
        Export(ExportArgs),
808
        /// Export data for all stages to JSON files
809
        ExportAll(ExportAllArgs),
810
        /// Import stage data from a JSON file
811
        Import(ImportArgs),
812
        /// Import all stages from JSON files
813
        ImportAll(ImportAllArgs),
814
    }
815

816
    #[derive(Args)]
483✔
817
    pub struct ExportArgs {
818
        /// Name of the stage to export
819
        pub stage: String,
×
820

821
        /// Redirect output to a file instead of stdout
822
        #[clap(short, value_name("PATH"))]
823
        pub output: Option<PathBuf>,
824
    }
825

826
    #[derive(Args)]
483✔
827
    pub struct ExportAllArgs {
828
        /// Path to the output directory
829
        #[clap(short, value_name("PATH"))]
830
        pub output: PathBuf,
×
831
    }
832

833
    #[derive(Args)]
483✔
834
    pub struct ImportArgs {
835
        /// Name of the stage to import
836
        pub stage: String,
×
837

838
        /// Path to the input JSON file
839
        pub input: PathBuf,
×
840
    }
841

842
    #[derive(Args)]
483✔
843
    pub struct ImportAllArgs {
844
        /// Path to the input directory
845
        pub input: PathBuf,
×
846

847
        /// Always import a stage even if it hasn't changed
848
        #[clap(short, long)]
849
        pub force: bool,
×
850
    }
851
}
852

853
#[cfg(test)]
854
mod tests {
855
    use super::*;
856
    use clap::error::ErrorKind;
857
    use float_cmp::approx_eq;
858
    use std::ffi::OsString;
859
    use std::iter;
860
    use std::path::Path;
861

862
    /// Generates a mapping function which pattern-matches a parsed command
863
    macro_rules! mapper {
864
        ($p:pat => $out:expr) => {
865
            |a: CliArgs| {
866
                let $p = a.command else { panic!() };
867
                $out
868
            }
869
        };
870
    }
871

872
    /// Parses `args` using an argument parser, maps the results using `mapper`, and passes the
873
    /// final value to `predicate`.
874
    fn parse<O, S, I, M, A, F>(args: I, mapper: M, predicate: F)
297✔
875
    where
297✔
876
        O: Parser,
297✔
877
        S: Into<OsString> + Clone,
297✔
878
        I: IntoIterator<Item = S>,
297✔
879
        M: FnOnce(O) -> A,
297✔
880
        F: FnOnce(A),
297✔
881
    {
297✔
882
        let parsed = O::try_parse_from(
297✔
883
            iter::once(OsString::new()).chain(args.into_iter().map(|a| a.into())),
993✔
884
        )
297✔
885
        .unwrap();
297✔
886
        predicate(mapper(parsed));
297✔
887
    }
297✔
888

889
    /// Parses each list of arguments in `argsets` using an argument parser, maps each result using
890
    /// `mapper`, and calls `predicate` for each value.
891
    fn multiparse<O, S, I, J, M, A, F>(argsets: J, mapper: M, predicate: F)
45✔
892
    where
45✔
893
        O: Parser,
45✔
894
        S: Into<OsString> + Clone,
45✔
895
        I: IntoIterator<Item = S>,
45✔
896
        J: IntoIterator<Item = I>,
45✔
897
        M: Fn(O) -> A,
45✔
898
        F: Fn(A),
45✔
899
    {
45✔
900
        for args in argsets {
144✔
901
            let parsed = O::try_parse_from(
99✔
902
                iter::once(OsString::new()).chain(args.into_iter().map(|a| a.into())),
255✔
903
            )
99✔
904
            .unwrap();
99✔
905
            predicate(mapper(parsed));
99✔
906
        }
99✔
907
    }
45✔
908

909
    fn error(args: impl IntoIterator<Item = &'static str>) -> ErrorKind {
138✔
910
        CliArgs::try_parse_from(iter::once("unplug").chain(args)).err().expect("error").kind()
138✔
911
    }
138✔
912

913
    #[test]
3✔
914
    fn test_cli_empty() {
3✔
915
        assert_eq!(error([]), ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand);
3✔
916
    }
3✔
917

918
    #[test]
3✔
919
    fn test_cli_global_options() {
3✔
920
        let map = std::convert::identity::<CliArgs>;
3✔
921
        parse(["-v", "test"], map, |args| {
3✔
922
            assert_eq!(args.verbose, 1);
3✔
923
        });
3✔
924
        parse(["-vv", "test"], map, |args| {
3✔
925
            assert_eq!(args.verbose, 2);
3✔
926
        });
3✔
927
        parse(["--config", "foo", "test"], map, |args| {
3✔
928
            assert_eq!(args.config.config.as_deref(), Some(Path::new("foo")));
3✔
929
        });
3✔
930
        parse(["--no-config", "test"], map, |args| {
3✔
931
            assert!(args.config.no_config);
3✔
932
        });
3✔
933
        assert_eq!(error(["--config", "foo", "--no-config", "test"]), ErrorKind::ArgumentConflict);
3✔
934
        parse(["--iso", "foo", "test"], map, |args| {
3✔
935
            assert_eq!(args.context.iso.as_deref(), Some(Path::new("foo")));
3✔
936
        });
3✔
937
        parse(["--default-iso", "test"], map, |args| {
3✔
938
            assert!(args.context.default_iso);
3✔
939
        });
3✔
940
        multiparse([["-p", "foo", "test"], ["--project", "foo", "test"]], map, |args| {
6✔
941
            assert_eq!(args.context.project.as_deref(), Some("foo"));
6✔
942
        });
6✔
943
        assert_eq!(error(["--iso", "foo", "--default-iso", "test"]), ErrorKind::ArgumentConflict);
3✔
944
        assert_eq!(error(["--iso", "foo", "-p", "bar", "test"]), ErrorKind::ArgumentConflict);
3✔
945
        assert_eq!(error(["--default-iso", "-p", "bar", "test"]), ErrorKind::ArgumentConflict);
3✔
946
    }
3✔
947

948
    #[test]
3✔
949
    fn test_cli_list_options() {
3✔
950
        use list::*;
3✔
951
        #[derive(Parser)]
36✔
952
        struct ListOptionsParser {
3✔
953
            #[clap(flatten)]
3✔
954
            inner: Options,
3✔
955
        }
3✔
956
        let map = |o: ListOptionsParser| o.inner;
36✔
957
        parse(["--reverse"], map, |args| {
3✔
958
            assert!(args.reverse);
3✔
959
        });
3✔
960
        multiparse([["-l"], ["--long"]], map, |args| {
6✔
961
            assert!(args.long);
6✔
962
        });
6✔
963
        multiparse(
3✔
964
            [&["--by-name"][..], &["--by-offset", "--by-name"], &["--by-size", "--by-name"]],
3✔
965
            map,
3✔
966
            |args| {
9✔
967
                assert!(args.by_name);
9✔
968
                assert!(!args.by_offset);
9✔
969
                assert!(!args.by_size);
9✔
970
            },
9✔
971
        );
3✔
972
        multiparse(
3✔
973
            [&["--by-offset"][..], &["--by-name", "--by-offset"], &["--by-size", "--by-offset"]],
3✔
974
            map,
3✔
975
            |args| {
9✔
976
                assert!(!args.by_name);
9✔
977
                assert!(args.by_offset);
9✔
978
                assert!(!args.by_size);
9✔
979
            },
9✔
980
        );
3✔
981
        multiparse(
3✔
982
            [&["--by-size"][..], &["--by-name", "--by-size"], &["--by-offset", "--by-size"]],
3✔
983
            map,
3✔
984
            |args| {
9✔
985
                assert!(!args.by_name);
9✔
986
                assert!(!args.by_offset);
9✔
987
                assert!(args.by_size);
9✔
988
            },
9✔
989
        );
3✔
990
    }
3✔
991

992
    #[test]
3✔
993
    fn test_cli_list_ids_args() {
3✔
994
        use list::*;
3✔
995
        #[derive(Parser)]
15✔
996
        struct ListIdArgsParser {
3✔
997
            #[clap(flatten)]
3✔
998
            inner: IdArgs,
3✔
999
        }
3✔
1000
        let map = |o: ListIdArgsParser| o.inner;
15✔
1001
        parse(["--reverse"], map, |args| {
3✔
1002
            assert!(args.reverse);
3✔
1003
        });
3✔
1004
        multiparse([&["--by-name"][..], &["--by-id", "--by-name"]], map, |args| {
6✔
1005
            assert!(args.by_name);
6✔
1006
            assert!(!args.by_id);
6✔
1007
        });
6✔
1008
        multiparse([&["--by-id"][..], &["--by-name", "--by-id"]], map, |args| {
6✔
1009
            assert!(!args.by_name);
6✔
1010
            assert!(args.by_id);
6✔
1011
        });
6✔
1012
    }
3✔
1013

1014
    #[test]
3✔
1015
    fn test_cli_archive_info() {
3✔
1016
        use archive::*;
3✔
1017
        let map = mapper!(Command::Archive(Subcommand::Info { path }) => path);
3✔
1018
        parse(["archive", "info", "foo"], map, |path| {
3✔
1019
            assert_eq!(path, "foo");
3✔
1020
        });
3✔
1021
        assert_eq!(error(["archive", "info"]), ErrorKind::MissingRequiredArgument);
3✔
1022
    }
3✔
1023

1024
    #[test]
3✔
1025
    fn test_cli_archive_list() {
3✔
1026
        use archive::*;
3✔
1027
        let map = mapper!(Command::Archive(Subcommand::List { path, args }) => (path, args));
9✔
1028
        parse(["archive", "list", "qp.bin"], map, |(path, args)| {
3✔
1029
            assert_eq!(path, "qp.bin");
3✔
1030
            assert!(args.paths.is_empty());
3✔
1031
        });
3✔
1032
        parse(["archive", "list", "qp.bin", "foo"], map, |(path, args)| {
3✔
1033
            assert_eq!(path, "qp.bin");
3✔
1034
            assert_eq!(args.paths, ["foo"]);
3✔
1035
        });
3✔
1036
        parse(["archive", "list", "qp.bin", "foo", "bar", "--long"], map, |(path, args)| {
3✔
1037
            assert_eq!(path, "qp.bin");
3✔
1038
            assert_eq!(args.paths, ["foo", "bar"]);
3✔
1039
            assert!(args.settings.long);
3✔
1040
        });
3✔
1041
        assert_eq!(error(["archive", "list"]), ErrorKind::MissingRequiredArgument);
3✔
1042
    }
3✔
1043

1044
    #[test]
3✔
1045
    fn test_cli_archive_extract() {
3✔
1046
        use archive::*;
3✔
1047
        let map = mapper!(Command::Archive(Subcommand::Extract { path, args }) => (path, args));
6✔
1048
        parse(["archive", "extract", "qp.bin"], map, |(path, args)| {
3✔
1049
            assert_eq!(path, "qp.bin");
3✔
1050
            assert_eq!(args.output, None);
3✔
1051
            assert!(args.paths.is_empty());
3✔
1052
        });
3✔
1053
        parse(["archive", "extract", "qp.bin", "-o", "out", "foo", "bar"], map, |(path, args)| {
3✔
1054
            assert_eq!(path, "qp.bin");
3✔
1055
            assert_eq!(args.output, Some("out".into()));
3✔
1056
            assert_eq!(args.paths, ["foo", "bar"]);
3✔
1057
        });
3✔
1058
        assert_eq!(error(["archive", "extract"]), ErrorKind::MissingRequiredArgument);
3✔
1059
    }
3✔
1060

1061
    #[test]
3✔
1062
    fn test_cli_archive_extract_all() {
3✔
1063
        use archive::*;
3✔
1064
        let map = mapper!(Command::Archive(Subcommand::ExtractAll { path, args }) => (path, args));
6✔
1065
        parse(["archive", "extract-all", "qp.bin"], map, |(path, args)| {
3✔
1066
            assert_eq!(path, "qp.bin");
3✔
1067
            assert_eq!(args.output, None);
3✔
1068
        });
3✔
1069
        parse(["archive", "extract-all", "qp.bin", "-o", "out"], map, |(path, args)| {
3✔
1070
            assert_eq!(path, "qp.bin");
3✔
1071
            assert_eq!(args.output, Some("out".into()));
3✔
1072
        });
3✔
1073
        assert_eq!(error(["archive", "extract-all"]), ErrorKind::MissingRequiredArgument);
3✔
1074
    }
3✔
1075

1076
    #[test]
3✔
1077
    fn test_cli_archive_replace() {
3✔
1078
        use archive::*;
3✔
1079
        let map = mapper!(Command::Archive(Subcommand::Replace { path, args }) => (path, args));
3✔
1080
        parse(["archive", "replace", "qp.bin", "foo", "bar"], map, |(path, args)| {
3✔
1081
            assert_eq!(path, "qp.bin");
3✔
1082
            assert_eq!(args.dest_path, "foo");
3✔
1083
            assert_eq!(args.src_path, Path::new("bar"));
3✔
1084
        });
3✔
1085
        assert_eq!(error(["archive", "replace"]), ErrorKind::MissingRequiredArgument);
3✔
1086
        assert_eq!(error(["archive", "replace", "qp.bin"]), ErrorKind::MissingRequiredArgument);
3✔
1087
        assert_eq!(
3✔
1088
            error(["archive", "replace", "qp.bin", "foo"]),
3✔
1089
            ErrorKind::MissingRequiredArgument
3✔
1090
        );
3✔
1091
    }
3✔
1092

1093
    #[test]
3✔
1094
    fn test_cli_audio_info() {
3✔
1095
        use audio::*;
3✔
1096
        let map = mapper!(Command::Audio(Subcommand::Info(args)) => args);
3✔
1097
        parse(["audio", "info", "foo"], map, |args| {
3✔
1098
            assert_eq!(args.name, "foo");
3✔
1099
        });
3✔
1100
        assert_eq!(error(["audio", "info"]), ErrorKind::MissingRequiredArgument);
3✔
1101
    }
3✔
1102

1103
    #[test]
3✔
1104
    fn test_cli_audio_export() {
3✔
1105
        use audio::*;
3✔
1106
        let map = mapper!(Command::Audio(Subcommand::Export(args)) => args);
9✔
1107
        parse(["audio", "export", "foo"], map, |args| {
3✔
1108
            assert_eq!(args.output, None);
3✔
1109
            assert_eq!(args.names, ["foo"]);
3✔
1110
        });
3✔
1111
        parse(["audio", "export", "foo", "bar", "baz"], map, |args| {
3✔
1112
            assert_eq!(args.output, None);
3✔
1113
            assert_eq!(args.names, ["foo", "bar", "baz"]);
3✔
1114
        });
3✔
1115
        parse(["audio", "export", "-o", "out", "--labels", "foo"], map, |args| {
3✔
1116
            assert_eq!(args.output.as_deref(), Some(Path::new("out")));
3✔
1117
            assert!(args.settings.labels);
3✔
1118
            assert_eq!(args.names, ["foo"]);
3✔
1119
        });
3✔
1120
        assert_eq!(error(["audio", "export"]), ErrorKind::MissingRequiredArgument);
3✔
1121
    }
3✔
1122

1123
    #[test]
3✔
1124
    fn test_cli_audio_export_bank() {
3✔
1125
        use audio::*;
3✔
1126
        let map = mapper!(Command::Audio(Subcommand::ExportBank(args)) => args);
6✔
1127
        parse(["audio", "export-bank", "foo"], map, |args| {
3✔
1128
            assert_eq!(args.output, None);
3✔
1129
            assert_eq!(args.name, "foo");
3✔
1130
        });
3✔
1131
        parse(["audio", "export-bank", "-o", "out", "--labels", "foo"], map, |args| {
3✔
1132
            assert_eq!(args.output.as_deref(), Some(Path::new("out")));
3✔
1133
            assert!(args.settings.labels);
3✔
1134
            assert_eq!(args.name, "foo");
3✔
1135
        });
3✔
1136
        assert_eq!(error(["audio", "export-bank"]), ErrorKind::MissingRequiredArgument);
3✔
1137
        assert_eq!(error(["audio", "export-bank", "foo", "bar"]), ErrorKind::UnknownArgument);
3✔
1138
    }
3✔
1139

1140
    #[test]
3✔
1141
    fn test_cli_audio_export_all() {
3✔
1142
        use audio::*;
3✔
1143
        let map = mapper!(Command::Audio(Subcommand::ExportAll(args)) => args);
6✔
1144
        parse(["audio", "export-all", "-o", "out"], map, |args| {
3✔
1145
            assert_eq!(args.output, Path::new("out"));
3✔
1146
            assert!(!args.settings.labels);
3✔
1147
        });
3✔
1148
        parse(["audio", "export-all", "-o", "out", "--labels"], map, |args| {
3✔
1149
            assert_eq!(args.output, Path::new("out"));
3✔
1150
            assert!(args.settings.labels);
3✔
1151
        });
3✔
1152
        assert_eq!(error(["audio", "export-all"]), ErrorKind::MissingRequiredArgument);
3✔
1153
    }
3✔
1154

1155
    #[test]
3✔
1156
    fn test_cli_audio_import() {
3✔
1157
        use audio::*;
3✔
1158
        let map = mapper!(Command::Audio(Subcommand::Import(args)) => args);
6✔
1159
        parse(["audio", "import", "foo", "bar"], map, |args| {
3✔
1160
            assert_eq!(args.name, "foo");
3✔
1161
            assert_eq!(args.path, Path::new("bar"));
3✔
1162
            assert!(!args.settings.labels);
3✔
1163
        });
3✔
1164
        parse(["audio", "import", "foo", "bar", "--labels"], map, |args| {
3✔
1165
            assert_eq!(args.name, "foo");
3✔
1166
            assert_eq!(args.path, Path::new("bar"));
3✔
1167
            assert!(args.settings.labels);
3✔
1168
        });
3✔
1169
        assert_eq!(error(["audio", "import"]), ErrorKind::MissingRequiredArgument);
3✔
1170
        assert_eq!(error(["audio", "import", "foo"]), ErrorKind::MissingRequiredArgument);
3✔
1171
        assert_eq!(error(["audio", "import", "foo", "bar", "baz"]), ErrorKind::UnknownArgument);
3✔
1172
    }
3✔
1173

1174
    #[test]
3✔
1175
    fn test_cli_audio_play() {
3✔
1176
        use audio::*;
3✔
1177
        let map = mapper!(Command::Audio(Subcommand::Play(args)) => args);
12✔
1178
        parse(["audio", "play", "foo"], map, |args| {
3✔
1179
            assert_eq!(args.name, "foo");
3✔
1180
            assert!(approx_eq!(f64, args.volume, 0.8));
3✔
1181
        });
3✔
1182
        parse(["audio", "play", "foo", "--volume", "0"], map, |args| {
3✔
1183
            assert!(approx_eq!(f64, args.volume, 0.0));
3✔
1184
        });
3✔
1185
        parse(["audio", "play", "foo", "--volume", "50"], map, |args| {
3✔
1186
            assert!(approx_eq!(f64, args.volume, 0.5));
3✔
1187
        });
3✔
1188
        parse(["audio", "play", "foo", "--volume", "100"], map, |args| {
3✔
1189
            assert!(approx_eq!(f64, args.volume, 1.0));
3✔
1190
        });
3✔
1191
        assert_eq!(error(["audio", "play"]), ErrorKind::MissingRequiredArgument);
3✔
1192
        assert_eq!(error(["audio", "play", "foo", "bar"]), ErrorKind::UnknownArgument);
3✔
1193
        assert_eq!(error(["audio", "play", "foo", "--volume", "-1"]), ErrorKind::ValueValidation);
3✔
1194
        assert_eq!(error(["audio", "play", "foo", "--volume", "101"]), ErrorKind::ValueValidation);
3✔
1195
    }
3✔
1196

1197
    #[test]
3✔
1198
    fn test_cli_config() {
3✔
1199
        use config::*;
3✔
1200
        let map = std::convert::identity;
3✔
1201
        parse(["config", "clear"], map, |a: CliArgs| {
3✔
1202
            assert!(matches!(a.command, Command::Config(Subcommand::Clear)));
3✔
1203
        });
3✔
1204
        parse(["config", "path"], map, |a: CliArgs| {
3✔
1205
            assert!(matches!(a.command, Command::Config(Subcommand::Path)));
3✔
1206
        });
3✔
1207
        parse(["config", "get", "default-iso"], map, |a: CliArgs| {
3✔
1208
            let Command::Config(Subcommand::Get(args)) = a.command else { panic!() };
3✔
1209
            assert!(matches!(args, GetSetting::DefaultIso));
3✔
1210
        });
3✔
1211
        parse(["config", "get", "dolphin-path"], map, |a: CliArgs| {
3✔
1212
            let Command::Config(Subcommand::Get(args)) = a.command else { panic!() };
3✔
1213
            assert!(matches!(args, GetSetting::DolphinPath));
3✔
1214
        });
3✔
1215
        parse(["config", "set", "default-iso"], map, |a: CliArgs| {
3✔
1216
            let Command::Config(Subcommand::Set(SetSetting::DefaultIso { path })) = a.command
3✔
1217
            else {
1218
                panic!()
×
1219
            };
1220
            assert_eq!(path, None);
3✔
1221
        });
3✔
1222
        parse(["config", "set", "default-iso", "foo"], map, |a: CliArgs| {
3✔
1223
            let Command::Config(Subcommand::Set(SetSetting::DefaultIso { path })) = a.command
3✔
1224
            else {
1225
                panic!()
×
1226
            };
1227
            assert_eq!(path.as_deref(), Some("foo"));
3✔
1228
        });
3✔
1229
        parse(["config", "set", "dolphin-path"], map, |a: CliArgs| {
3✔
1230
            let Command::Config(Subcommand::Set(SetSetting::DolphinPath { path })) = a.command
3✔
1231
            else {
1232
                panic!()
×
1233
            };
1234
            assert_eq!(path, None);
3✔
1235
        });
3✔
1236
        parse(["config", "set", "dolphin-path", "foo"], map, |a: CliArgs| {
3✔
1237
            let Command::Config(Subcommand::Set(SetSetting::DolphinPath { path })) = a.command
3✔
1238
            else {
1239
                panic!()
×
1240
            };
1241
            assert_eq!(path.as_deref(), Some("foo"));
3✔
1242
        });
3✔
1243
        assert_eq!(error(["config", "get"]), ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand);
3✔
1244
        assert_eq!(error(["config", "set"]), ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand);
3✔
1245
    }
3✔
1246

1247
    #[test]
3✔
1248
    fn test_cli_dolphin() {
3✔
1249
        let map = mapper!(Command::Dolphin(args) => args);
12✔
1250
        parse(["dolphin"], map, |args| {
3✔
1251
            assert!(!args.wait);
3✔
1252
            assert!(!args.ui);
3✔
1253
        });
3✔
1254
        parse(["dolphin", "--ui"], map, |args| {
3✔
1255
            assert!(args.ui);
3✔
1256
        });
3✔
1257
        multiparse([["dolphin", "-w"], ["dolphin", "--wait"]], map, |args| {
6✔
1258
            assert!(args.wait);
6✔
1259
        });
6✔
1260
    }
3✔
1261

1262
    #[test]
3✔
1263
    fn test_cli_globals_export() {
3✔
1264
        use globals::*;
3✔
1265
        let map = mapper!(Command::Globals(Subcommand::Export(args)) => args);
12✔
1266
        parse(["globals", "export"], map, |args| {
3✔
1267
            assert!(!args.compact);
3✔
1268
            assert_eq!(args.output, None);
3✔
1269
        });
3✔
1270
        multiparse(
3✔
1271
            [["globals", "export", "-c"], ["globals", "export", "--compact"]],
3✔
1272
            map,
3✔
1273
            |args| {
6✔
1274
                assert!(args.compact);
6✔
1275
            },
6✔
1276
        );
3✔
1277
        parse(["globals", "export", "-o", "out"], map, |args| {
3✔
1278
            assert_eq!(args.output.as_deref(), Some(Path::new("out")));
3✔
1279
        });
3✔
1280
    }
3✔
1281

1282
    #[test]
3✔
1283
    fn test_cli_globals_import() {
3✔
1284
        use globals::*;
3✔
1285
        let map = mapper!(Command::Globals(Subcommand::Import(args)) => args);
3✔
1286
        parse(["globals", "import", "foo"], map, |args| {
3✔
1287
            assert_eq!(args.input, Path::new("foo"));
3✔
1288
        });
3✔
1289
        assert_eq!(error(["globals", "import"]), ErrorKind::MissingRequiredArgument);
3✔
1290
    }
3✔
1291

1292
    #[test]
3✔
1293
    fn test_cli_globals_dump_colliders() {
3✔
1294
        use globals::*;
3✔
1295
        let map = mapper!(Command::Globals(Subcommand::DumpColliders(args)) => args);
6✔
1296
        parse(["globals", "dump-colliders"], map, |args| {
3✔
1297
            assert_eq!(args.output, None);
3✔
1298
        });
3✔
1299
        parse(["globals", "dump-colliders", "-o", "out"], map, |args| {
3✔
1300
            assert_eq!(args.output.as_deref(), Some(Path::new("out")));
3✔
1301
        });
3✔
1302
    }
3✔
1303

1304
    #[test]
3✔
1305
    fn test_cli_iso_info() {
3✔
1306
        use iso::*;
3✔
1307
        let map = mapper!(Command::Iso(c) => c);
3✔
1308
        parse(["iso", "info"], map, |c| {
3✔
1309
            assert!(matches!(c, Subcommand::Info));
3✔
1310
        });
3✔
1311
    }
3✔
1312

1313
    #[test]
3✔
1314
    fn test_cli_iso_list() {
3✔
1315
        use iso::*;
3✔
1316
        let map = mapper!(Command::Iso(Subcommand::List(args)) => args);
6✔
1317
        parse(["iso", "list"], map, |args| {
3✔
1318
            assert!(args.paths.is_empty());
3✔
1319
        });
3✔
1320
        parse(["iso", "list", "foo", "bar", "--long"], map, |args| {
3✔
1321
            assert_eq!(args.paths, ["foo", "bar"]);
3✔
1322
            assert!(args.settings.long);
3✔
1323
        });
3✔
1324
    }
3✔
1325

1326
    #[test]
3✔
1327
    fn test_cli_iso_extract() {
3✔
1328
        use iso::*;
3✔
1329
        let map = mapper!(Command::Iso(Subcommand::Extract(args)) => args);
6✔
1330
        parse(["iso", "extract"], map, |args| {
3✔
1331
            assert_eq!(args.output, None);
3✔
1332
            assert!(args.paths.is_empty());
3✔
1333
        });
3✔
1334
        parse(["iso", "extract", "foo", "bar", "-o", "out"], map, |args| {
3✔
1335
            assert_eq!(args.output.as_deref(), Some(Path::new("out")));
3✔
1336
            assert_eq!(args.paths, ["foo", "bar"]);
3✔
1337
        });
3✔
1338
    }
3✔
1339

1340
    #[test]
3✔
1341
    fn test_cli_iso_extract_all() {
3✔
1342
        use iso::*;
3✔
1343
        let map = mapper!(Command::Iso(Subcommand::ExtractAll(args)) => args);
6✔
1344
        parse(["iso", "extract-all"], map, |args| {
3✔
1345
            assert_eq!(args.output, None);
3✔
1346
        });
3✔
1347
        parse(["iso", "extract-all", "-o", "out"], map, |args| {
3✔
1348
            assert_eq!(args.output.as_deref(), Some(Path::new("out")));
3✔
1349
        });
3✔
1350
    }
3✔
1351

1352
    #[test]
3✔
1353
    fn test_cli_iso_replace() {
3✔
1354
        use iso::*;
3✔
1355
        let map = mapper!(Command::Iso(Subcommand::Replace(args)) => args);
3✔
1356
        parse(["iso", "replace", "foo", "bar"], map, |args| {
3✔
1357
            assert_eq!(args.dest_path, "foo");
3✔
1358
            assert_eq!(args.src_path, Path::new("bar"));
3✔
1359
        });
3✔
1360
        assert_eq!(error(["iso", "replace"]), ErrorKind::MissingRequiredArgument);
3✔
1361
        assert_eq!(error(["iso", "replace", "foo"]), ErrorKind::MissingRequiredArgument);
3✔
1362
        assert_eq!(error(["iso", "replace", "foo", "bar", "baz"]), ErrorKind::UnknownArgument);
3✔
1363
    }
3✔
1364

1365
    #[test]
3✔
1366
    fn test_cli_iso_set() {
3✔
1367
        use iso::*;
3✔
1368
        let map = mapper!(Command::Iso(Subcommand::Set(command)) => command);
6✔
1369
        parse(["iso", "set", "maker", "The"], map, |command| {
3✔
1370
            let SetCommand::Maker { name } = command else { panic!() };
3✔
1371
            assert_eq!(name, "The");
3✔
1372
        });
3✔
1373
        parse(["iso", "set", "name", "This"], map, |command| {
3✔
1374
            let SetCommand::Name { name } = command else { panic!() };
3✔
1375
            assert_eq!(name, "This");
3✔
1376
        });
3✔
1377
    }
3✔
1378

1379
    #[test]
3✔
1380
    fn test_cli_list() {
3✔
1381
        use list::*;
3✔
1382
        let map = mapper!(Command::List(c) => c);
24✔
1383
        parse(["list", "items"], map, |c| {
3✔
1384
            let Subcommand::Items(args) = c else { panic!() };
3✔
1385
            assert!(!args.show_unknown);
3✔
1386
        });
3✔
1387
        parse(["list", "items", "--show-unknown"], map, |c| {
3✔
1388
            let Subcommand::Items(args) = c else { panic!() };
3✔
1389
            assert!(args.show_unknown);
3✔
1390
        });
3✔
1391
        parse(["list", "equipment"], map, |c| {
3✔
1392
            let Subcommand::Equipment(args) = c else { panic!() };
3✔
1393
            assert!(!args.show_unknown);
3✔
1394
        });
3✔
1395
        parse(["list", "equipment", "--show-unknown"], map, |c| {
3✔
1396
            let Subcommand::Equipment(args) = c else { panic!() };
3✔
1397
            assert!(args.show_unknown);
3✔
1398
        });
3✔
1399
        parse(["list", "stages"], map, |c| {
3✔
1400
            assert!(matches!(c, Subcommand::Stages(_)));
3✔
1401
        });
3✔
1402
        parse(["list", "objects"], map, |c| {
3✔
1403
            assert!(matches!(c, Subcommand::Objects(_)));
3✔
1404
        });
3✔
1405
        parse(["list", "music"], map, |c| {
3✔
1406
            assert!(matches!(c, Subcommand::Music(_)));
3✔
1407
        });
3✔
1408
        parse(["list", "sounds"], map, |c| {
3✔
1409
            assert!(matches!(c, Subcommand::Sounds(_)));
3✔
1410
        });
3✔
1411
    }
3✔
1412

1413
    #[test]
3✔
1414
    fn test_cli_messages() {
3✔
1415
        use messages::*;
3✔
1416
        let map = mapper!(Command::Messages(c) => c);
6✔
1417
        parse(["messages", "export", "-o", "out"], map, |c| {
3✔
1418
            let Subcommand::Export(args) = c else { panic!() };
3✔
1419
            assert_eq!(args.output, Path::new("out"));
3✔
1420
        });
3✔
1421
        parse(["messages", "import", "foo"], map, |c| {
3✔
1422
            let Subcommand::Import(args) = c else { panic!() };
3✔
1423
            assert_eq!(args.input, Path::new("foo"));
3✔
1424
        });
3✔
1425
    }
3✔
1426

1427
    #[test]
3✔
1428
    fn test_cli_project_info() {
3✔
1429
        use project::*;
3✔
1430
        let map = mapper!(Command::Project(Subcommand::Info(args)) => args);
6✔
1431
        parse(["project", "info"], map, |args| {
3✔
1432
            assert_eq!(args.name, None);
3✔
1433
        });
3✔
1434
        parse(["project", "info", "foo"], map, |args| {
3✔
1435
            assert_eq!(args.name.as_deref(), Some("foo"));
3✔
1436
        });
3✔
1437
    }
3✔
1438

1439
    #[test]
3✔
1440
    fn test_cli_project_list() {
3✔
1441
        use project::*;
3✔
1442
        let map = std::convert::identity;
3✔
1443
        parse(["project", "list"], map, |args: CliArgs| {
3✔
1444
            assert!(matches!(args.command, Command::Project(Subcommand::List)));
3✔
1445
        });
3✔
1446
        assert_eq!(error(["project", "list", "foo"]), ErrorKind::UnknownArgument);
3✔
1447
    }
3✔
1448

1449
    #[test]
3✔
1450
    fn test_cli_project_new() {
3✔
1451
        use project::*;
3✔
1452
        let map = mapper!(Command::Project(Subcommand::New(args)) => args);
18✔
1453
        parse(["project", "new", "foo"], map, |args| {
3✔
1454
            assert_eq!(args.name, "foo");
3✔
1455
            assert_eq!(args.source, None);
3✔
1456
            assert_eq!(args.output, None);
3✔
1457
            assert!(!args.force);
3✔
1458
            assert!(!args.no_open);
3✔
1459
        });
3✔
1460
        parse(["project", "new", "foo", "-s", "src"], map, |args| {
3✔
1461
            assert_eq!(args.name, "foo");
3✔
1462
            assert_eq!(args.source.as_deref(), Some(Path::new("src")));
3✔
1463
        });
3✔
1464
        parse(["project", "new", "foo", "-o", "out"], map, |args| {
3✔
1465
            assert_eq!(args.name, "foo");
3✔
1466
            assert_eq!(args.output.as_deref(), Some(Path::new("out")));
3✔
1467
        });
3✔
1468
        parse(["project", "new", "foo", "--no-open"], map, |args| {
3✔
1469
            assert_eq!(args.name, "foo");
3✔
1470
            assert!(args.no_open);
3✔
1471
        });
3✔
1472
        multiparse(
3✔
1473
            [["project", "new", "foo", "-f"], ["project", "new", "foo", "--force"]],
3✔
1474
            map,
3✔
1475
            |args| {
6✔
1476
                assert_eq!(args.name, "foo");
6✔
1477
                assert!(args.force);
6✔
1478
            },
6✔
1479
        );
3✔
1480
        assert_eq!(error(["project", "new"]), ErrorKind::MissingRequiredArgument);
3✔
1481
    }
3✔
1482

1483
    #[test]
3✔
1484
    fn test_cli_project_wipe() {
3✔
1485
        use project::*;
3✔
1486
        let map = mapper!(Command::Project(Subcommand::Wipe(args)) => args);
9✔
1487
        parse(["project", "wipe", "foo"], map, |args| {
3✔
1488
            assert_eq!(args.name, "foo");
3✔
1489
            assert!(!args.force);
3✔
1490
        });
3✔
1491
        multiparse(
3✔
1492
            [["project", "wipe", "foo", "-f"], ["project", "wipe", "foo", "--force"]],
3✔
1493
            map,
3✔
1494
            |args| {
6✔
1495
                assert_eq!(args.name, "foo");
6✔
1496
                assert!(args.force);
6✔
1497
            },
6✔
1498
        );
3✔
1499
        assert_eq!(error(["project", "wipe"]), ErrorKind::MissingRequiredArgument);
3✔
1500
    }
3✔
1501

1502
    #[test]
3✔
1503
    fn test_cli_project_add() {
3✔
1504
        use project::*;
3✔
1505
        let map = mapper!(Command::Project(Subcommand::Add(args)) => args);
9✔
1506
        parse(["project", "add", "foo"], map, |args| {
3✔
1507
            assert_eq!(args.path, Path::new("foo"));
3✔
1508
            assert_eq!(args.name, None);
3✔
1509
        });
3✔
1510
        multiparse(
3✔
1511
            [["project", "add", "foo", "-n", "bar"], ["project", "add", "foo", "--name", "bar"]],
3✔
1512
            map,
3✔
1513
            |args| {
6✔
1514
                assert_eq!(args.path, Path::new("foo"));
6✔
1515
                assert_eq!(args.name.as_deref(), Some("bar"));
6✔
1516
            },
6✔
1517
        );
3✔
1518
        assert_eq!(error(["project", "add"]), ErrorKind::MissingRequiredArgument);
3✔
1519
    }
3✔
1520

1521
    #[test]
3✔
1522
    fn test_cli_project_remove() {
3✔
1523
        use project::*;
3✔
1524
        let map = mapper!(Command::Project(Subcommand::Remove(args)) => args);
6✔
1525
        multiparse([["project", "remove", "foo"], ["project", "forget", "foo"]], map, |args| {
6✔
1526
            assert_eq!(args.name, "foo");
6✔
1527
        });
6✔
1528
        assert_eq!(error(["project", "remove"]), ErrorKind::MissingRequiredArgument);
3✔
1529
        assert_eq!(error(["project", "remove", "foo", "bar"]), ErrorKind::UnknownArgument);
3✔
1530
    }
3✔
1531

1532
    #[test]
3✔
1533
    fn test_cli_project_open() {
3✔
1534
        use project::*;
3✔
1535
        let map = mapper!(Command::Project(Subcommand::Open(args)) => args);
3✔
1536
        parse(["project", "open", "foo"], map, |args| {
3✔
1537
            assert_eq!(args.name, "foo");
3✔
1538
        });
3✔
1539
        assert_eq!(error(["project", "open"]), ErrorKind::MissingRequiredArgument);
3✔
1540
        assert_eq!(error(["project", "open", "foo", "bar"]), ErrorKind::UnknownArgument);
3✔
1541
    }
3✔
1542

1543
    #[test]
3✔
1544
    fn test_cli_project_close() {
3✔
1545
        use project::*;
3✔
1546
        let map = mapper!(Command::Project(c) => c);
3✔
1547
        parse(["project", "close"], map, |c| {
3✔
1548
            assert!(matches!(c, Subcommand::Close));
3✔
1549
        });
3✔
1550
        assert_eq!(error(["project", "close", "foo"]), ErrorKind::UnknownArgument);
3✔
1551
    }
3✔
1552

1553
    #[test]
3✔
1554
    fn test_cli_qp() {
3✔
1555
        use archive::*;
3✔
1556
        let map = mapper!(Command::Qp(c) => c);
15✔
1557
        parse(["qp", "info"], map, |c| {
3✔
1558
            assert!(matches!(c, QpSubcommand::Info));
3✔
1559
        });
3✔
1560
        parse(["qp", "list"], map, |c| {
3✔
1561
            assert!(matches!(c, QpSubcommand::List(_)));
3✔
1562
        });
3✔
1563
        parse(["qp", "extract"], map, |c| {
3✔
1564
            assert!(matches!(c, QpSubcommand::Extract(_)));
3✔
1565
        });
3✔
1566
        parse(["qp", "extract-all"], map, |c| {
3✔
1567
            assert!(matches!(c, QpSubcommand::ExtractAll(_)));
3✔
1568
        });
3✔
1569
        parse(["qp", "replace", "foo", "bar"], map, |c| {
3✔
1570
            assert!(matches!(c, QpSubcommand::Replace(_)));
3✔
1571
        });
3✔
1572
    }
3✔
1573

1574
    #[test]
3✔
1575
    fn test_cli_script_disassemble() {
3✔
1576
        use script::*;
3✔
1577
        let map = mapper!(Command::Script(Subcommand::Disassemble(args)) => args);
3✔
1578
        parse(["script", "disassemble", "foo", "-o", "out"], map, |args| {
3✔
1579
            assert_eq!(args.stage, "foo");
3✔
1580
            assert_eq!(args.output, Path::new("out"));
3✔
1581
        });
3✔
1582
    }
3✔
1583

1584
    #[test]
3✔
1585
    fn test_cli_script_disassemble_all() {
3✔
1586
        use script::*;
3✔
1587
        let map = mapper!(Command::Script(Subcommand::DisassembleAll(args)) => args);
3✔
1588
        parse(["script", "disassemble-all", "-o", "out"], map, |args| {
3✔
1589
            assert_eq!(args.output, Path::new("out"));
3✔
1590
        });
3✔
1591
    }
3✔
1592

1593
    #[test]
3✔
1594
    fn test_cli_script_assemble() {
3✔
1595
        use script::*;
3✔
1596
        let map = mapper!(Command::Script(Subcommand::Assemble(args)) => args);
3✔
1597
        parse(["script", "assemble", "foo"], map, |args| {
3✔
1598
            assert_eq!(args.path, Path::new("foo"));
3✔
1599
        });
3✔
1600
    }
3✔
1601

1602
    #[test]
3✔
1603
    fn test_cli_shop_export() {
3✔
1604
        use shop::*;
3✔
1605
        let map = mapper!(Command::Shop(Subcommand::Export(args)) => args);
12✔
1606
        parse(["shop", "export"], map, |args| {
3✔
1607
            assert!(!args.compact);
3✔
1608
            assert_eq!(args.output, None);
3✔
1609
        });
3✔
1610
        parse(["shop", "export", "-o", "out"], map, |args| {
3✔
1611
            assert_eq!(args.output.as_deref(), Some(Path::new("out")));
3✔
1612
        });
3✔
1613
        multiparse([["shop", "export", "-c"], ["shop", "export", "--compact"]], map, |args| {
6✔
1614
            assert!(args.compact);
6✔
1615
        });
6✔
1616
    }
3✔
1617

1618
    #[test]
3✔
1619
    fn test_cli_shop_import() {
3✔
1620
        use shop::*;
3✔
1621
        let map = mapper!(Command::Shop(Subcommand::Import(args)) => args);
3✔
1622
        parse(["shop", "import", "foo"], map, |args| {
3✔
1623
            assert_eq!(args.input, Path::new("foo"));
3✔
1624
        });
3✔
1625
        assert_eq!(error(["shop", "import"]), ErrorKind::MissingRequiredArgument);
3✔
1626
    }
3✔
1627

1628
    #[test]
3✔
1629
    fn test_cli_stage_export() {
3✔
1630
        use stage::*;
3✔
1631
        let map = mapper!(Command::Stage(Subcommand::Export(args)) => args);
6✔
1632
        parse(["stage", "export", "foo"], map, |args| {
3✔
1633
            assert_eq!(args.stage, "foo");
3✔
1634
            assert_eq!(args.output, None);
3✔
1635
        });
3✔
1636
        parse(["stage", "export", "foo", "-o", "out"], map, |args| {
3✔
1637
            assert_eq!(args.stage, "foo");
3✔
1638
            assert_eq!(args.output.as_deref(), Some(Path::new("out")));
3✔
1639
        });
3✔
1640
        assert_eq!(error(["stage", "export"]), ErrorKind::MissingRequiredArgument);
3✔
1641
    }
3✔
1642

1643
    #[test]
3✔
1644
    fn test_cli_stage_export_all() {
3✔
1645
        use stage::*;
3✔
1646
        let map = mapper!(Command::Stage(Subcommand::ExportAll(args)) => args);
3✔
1647
        parse(["stage", "export-all", "-o", "out"], map, |args| {
3✔
1648
            assert_eq!(args.output, Path::new("out"));
3✔
1649
        });
3✔
1650
        assert_eq!(error(["stage", "export-all"]), ErrorKind::MissingRequiredArgument);
3✔
1651
    }
3✔
1652

1653
    #[test]
3✔
1654
    fn test_cli_stage_import() {
3✔
1655
        use stage::*;
3✔
1656
        let map = mapper!(Command::Stage(Subcommand::Import(args)) => args);
3✔
1657
        parse(["stage", "import", "foo", "bar"], map, |args| {
3✔
1658
            assert_eq!(args.stage, "foo");
3✔
1659
            assert_eq!(args.input, Path::new("bar"));
3✔
1660
        });
3✔
1661
        assert_eq!(error(["stage", "import"]), ErrorKind::MissingRequiredArgument);
3✔
1662
        assert_eq!(error(["stage", "import", "foo"]), ErrorKind::MissingRequiredArgument);
3✔
1663
    }
3✔
1664

1665
    #[test]
3✔
1666
    fn test_cli_stage_import_all() {
3✔
1667
        use stage::*;
3✔
1668
        let map = mapper!(Command::Stage(Subcommand::ImportAll(args)) => args);
9✔
1669
        parse(["stage", "import-all", "foo"], map, |args| {
3✔
1670
            assert_eq!(args.input, Path::new("foo"));
3✔
1671
            assert!(!args.force);
3✔
1672
        });
3✔
1673
        multiparse(
3✔
1674
            [["stage", "import-all", "foo", "-f"], ["stage", "import-all", "foo", "--force"]],
3✔
1675
            map,
3✔
1676
            |args| {
6✔
1677
                assert_eq!(args.input, Path::new("foo"));
6✔
1678
                assert!(args.force);
6✔
1679
            },
6✔
1680
        );
3✔
1681
        assert_eq!(error(["stage", "import-all"]), ErrorKind::MissingRequiredArgument);
3✔
1682
    }
3✔
1683

1684
    #[cfg(feature = "debug")]
1685
    #[test]
3✔
1686
    fn test_cli_debug_rebuild_scripts() {
3✔
1687
        use debug::*;
3✔
1688
        let map = mapper!(Command::Debug(c) => c);
3✔
1689
        parse(["debug", "rebuild-scripts"], map, |c| {
3✔
1690
            assert!(matches!(c, Subcommand::RebuildScripts));
3✔
1691
        });
3✔
1692
    }
3✔
1693

1694
    #[cfg(feature = "debug")]
1695
    #[test]
3✔
1696
    fn test_cli_debug_dump_script() {
3✔
1697
        use debug::*;
3✔
1698
        let map = mapper!(Command::Debug(Subcommand::DumpScript(args)) => args);
9✔
1699
        parse(["debug", "dump-script", "foo"], map, |args| {
3✔
1700
            assert_eq!(args.stage, "foo");
3✔
1701
            assert_eq!(args.output, None);
3✔
1702
            assert!(!args.flags.dump_unknown);
3✔
1703
            assert!(!args.flags.no_offsets);
3✔
1704
        });
3✔
1705
        parse(["debug", "dump-script", "foo", "-o", "out"], map, |args| {
3✔
1706
            assert_eq!(args.stage, "foo");
3✔
1707
            assert_eq!(args.output.as_deref(), Some(Path::new("out")));
3✔
1708
        });
3✔
1709
        parse(["debug", "dump-script", "foo", "--dump-unknown", "--no-offsets"], map, |args| {
3✔
1710
            assert_eq!(args.stage, "foo");
3✔
1711
            assert_eq!(args.output, None);
3✔
1712
            assert!(args.flags.dump_unknown);
3✔
1713
            assert!(args.flags.no_offsets);
3✔
1714
        });
3✔
1715
    }
3✔
1716

1717
    #[test]
3✔
1718
    fn test_cli_debug_dump_all_scripts() {
3✔
1719
        use debug::*;
3✔
1720
        let map = mapper!(Command::Debug(Subcommand::DumpAllScripts(args)) => args);
6✔
1721
        parse(["debug", "dump-all-scripts", "-o", "out"], map, |args| {
3✔
1722
            assert_eq!(args.output, Path::new("out"));
3✔
1723
        });
3✔
1724
        parse(
3✔
1725
            ["debug", "dump-all-scripts", "-o", "out", "--dump-unknown", "--no-offsets"],
3✔
1726
            map,
3✔
1727
            |args| {
3✔
1728
                assert_eq!(args.output, Path::new("out"));
3✔
1729
                assert!(args.flags.dump_unknown);
3✔
1730
                assert!(args.flags.no_offsets);
3✔
1731
            },
3✔
1732
        );
3✔
1733
        assert_eq!(error(["debug", "dump-all-scripts"]), ErrorKind::MissingRequiredArgument);
3✔
1734
    }
3✔
1735
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc