1use crate::xml::{
7 DecomposeRule, DisassembleXmlFileHandler, MultiLevelRule, ReassembleXmlFileHandler,
8};
9
10pub struct DisassembleOpts<'a> {
12 pub path: Option<&'a str>,
13 pub unique_id_elements: Option<&'a str>,
14 pub pre_purge: bool,
15 pub post_purge: bool,
16 pub ignore_path: Option<&'a str>,
22 pub format: &'a str,
23 pub strategy: Option<&'a str>,
24 pub multi_level: Option<String>,
25 pub split_tags: Option<String>,
26}
27
28pub fn parse_decompose_spec(spec: &str) -> Vec<DecomposeRule> {
32 let mut rules = Vec::new();
33 for part in spec.split(',') {
34 let part = part.trim();
35 let segments: Vec<&str> = part.splitn(4, ':').collect();
36 if segments.len() >= 3 {
37 let tag = segments[0].to_string();
38 let (path_segment, mode, field) = if segments.len() == 3 {
39 (
40 tag.clone(),
41 segments[1].to_string(),
42 segments[2].to_string(),
43 )
44 } else {
45 (
46 segments[1].to_string(),
47 segments[2].to_string(),
48 segments[3].to_string(),
49 )
50 };
51 if !tag.is_empty() && !mode.is_empty() && !field.is_empty() {
52 rules.push(DecomposeRule {
53 tag,
54 path_segment,
55 mode,
56 field,
57 });
58 }
59 }
60 }
61 rules
62}
63
64pub fn parse_multi_level_spec(spec: &str) -> Option<MultiLevelRule> {
66 let parts: Vec<&str> = spec.splitn(3, ':').collect();
67 if parts.len() != 3 {
68 return None;
69 }
70 let (file_pattern, root_to_strip, unique_id_elements) = (parts[0], parts[1], parts[2]);
71 if file_pattern.is_empty() || root_to_strip.is_empty() || unique_id_elements.is_empty() {
72 return None;
73 }
74 let path_segment = crate::xml::path_segment_from_file_pattern(file_pattern);
75 Some(MultiLevelRule {
76 file_pattern: file_pattern.to_string(),
77 root_to_strip: root_to_strip.to_string(),
78 unique_id_elements: unique_id_elements.to_string(),
79 path_segment: path_segment.clone(),
80 wrap_root_element: root_to_strip.to_string(),
81 wrap_xmlns: String::new(),
82 })
83}
84
85pub fn parse_multi_level_specs(spec: &str) -> Vec<MultiLevelRule> {
92 spec.split(';')
93 .map(str::trim)
94 .filter(|s| !s.is_empty())
95 .filter_map(parse_multi_level_spec)
96 .collect()
97}
98
99pub fn parse_disassemble_args(args: &[String]) -> DisassembleOpts<'_> {
113 let mut path = None;
114 let mut unique_id_elements = None;
115 let mut pre_purge = false;
116 let mut post_purge = false;
117 let mut ignore_path: Option<&str> = None;
118 let mut format = "xml";
119 let mut strategy = None;
120 let mut multi_level = None;
121 let mut split_tags = None;
122
123 let mut iter = args.iter();
124 while let Some(arg) = iter.next() {
125 if arg == "--postpurge" {
126 post_purge = true;
127 } else if arg == "--prepurge" {
128 pre_purge = true;
129 } else if let Some(rest) = arg.strip_prefix("--unique-id-elements=") {
130 unique_id_elements = Some(rest);
131 } else if arg == "--unique-id-elements" {
132 if let Some(value) = iter.next() {
133 unique_id_elements = Some(value.as_str());
134 }
135 } else if let Some(rest) = arg.strip_prefix("--ignore-path=") {
136 ignore_path = Some(rest);
137 } else if arg == "--ignore-path" {
138 if let Some(value) = iter.next() {
139 ignore_path = Some(value.as_str());
140 }
141 } else if let Some(rest) = arg.strip_prefix("--format=") {
142 format = rest;
143 } else if arg == "--format" {
144 if let Some(value) = iter.next() {
145 format = value.as_str();
146 }
147 } else if let Some(rest) = arg.strip_prefix("--strategy=") {
148 strategy = Some(rest);
149 } else if arg == "--strategy" {
150 if let Some(value) = iter.next() {
151 strategy = Some(value.as_str());
152 }
153 } else if let Some(rest) = arg.strip_prefix("--multi-level=") {
154 multi_level = Some(rest.to_string());
155 } else if arg == "--multi-level" {
156 if let Some(value) = iter.next() {
157 multi_level = Some(value.clone());
158 }
159 } else if let Some(rest) = arg.strip_prefix("--split-tags=") {
160 split_tags = Some(rest.to_string());
161 } else if arg == "--split-tags" || arg == "-p" {
162 if let Some(value) = iter.next() {
163 split_tags = Some(value.clone());
164 }
165 } else if arg.starts_with("--") {
166 } else if path.is_none() {
170 path = Some(arg.as_str());
171 }
172 }
176
177 DisassembleOpts {
178 path,
179 unique_id_elements,
180 pre_purge,
181 post_purge,
182 ignore_path,
183 format,
184 strategy,
185 multi_level,
186 split_tags,
187 }
188}
189
190pub fn parse_reassemble_args(args: &[String]) -> (Option<&str>, Option<&str>, bool) {
192 let mut path = None;
193 let mut extension = None;
194 let mut post_purge = false;
195 for arg in args {
196 if arg == "--postpurge" {
197 post_purge = true;
198 } else if path.is_none() {
199 path = Some(arg.as_str());
200 } else if extension.is_none() {
201 extension = Some(arg.as_str());
202 }
203 }
204 (path, extension, post_purge)
205}
206
207pub fn print_usage() {
209 eprintln!("Usage: xml-disassembler <command> [options]");
210 eprintln!(" disassemble <path> [options] - Disassemble XML file or directory");
211 eprintln!(" --postpurge - Delete original file/dir after disassembling (default: false)");
212 eprintln!(" --prepurge - Remove existing disassembly output before running (default: false)");
213 eprintln!(
214 " --unique-id-elements <list> - Comma-separated element names for nested filenames"
215 );
216 eprintln!(" --ignore-path <path> - Path to ignore file (default: .cdignore; falls back to .xmldisassemblerignore for backward compatibility)");
217 eprintln!(
218 " --format <fmt> - Output format: xml, json, json5, yaml (default: xml)"
219 );
220 eprintln!(
221 " --strategy <name> - unique-id or grouped-by-tag (default: unique-id)"
222 );
223 eprintln!(" --multi-level <spec> - Further disassemble matching files: file_pattern:root_to_strip:unique_id_elements (multiple rules separated by ';')");
224 eprintln!(" -p, --split-tags <spec> - With grouped-by-tag: split/group nested tags (e.g. objectPermissions:split:object,fieldPermissions:group:field)");
225 eprintln!(" reassemble <path> [extension] [--postpurge] - Reassemble directory (default extension: xml)");
226}
227
228fn should_print_usage(args_len: usize) -> bool {
232 args_len < 2
233}
234
235fn multi_level_spec_failed_to_parse(spec_present: bool, parsed_empty: bool) -> bool {
239 spec_present && parsed_empty
240}
241
242fn should_parse_decompose_rules(strategy: &str) -> bool {
247 strategy == "grouped-by-tag"
248}
249
250pub async fn run(args: Vec<String>) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
252 if should_print_usage(args.len()) {
253 print_usage();
254 return Ok(());
255 }
256
257 let command = &args[1];
258 match command.as_str() {
259 "disassemble" => run_disassemble(&args[2..]).await?,
260 "reassemble" => run_reassemble(&args[2..]).await?,
261 _ => {
262 eprintln!("Unknown command: {}", command);
263 }
264 }
265
266 Ok(())
267}
268
269async fn run_disassemble(args: &[String]) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
270 let opts = parse_disassemble_args(args);
271 let path = opts.path.unwrap_or(".");
272 let strategy = opts.strategy.unwrap_or("unique-id");
273 let multi_level_rules: Vec<MultiLevelRule> = opts
274 .multi_level
275 .as_deref()
276 .map(parse_multi_level_specs)
277 .unwrap_or_default();
278 if multi_level_spec_failed_to_parse(opts.multi_level.is_some(), multi_level_rules.is_empty()) {
279 eprintln!("Invalid --multi-level spec; use file_pattern:root_to_strip:unique_id_elements (multiple rules separated by ';')");
280 }
281 let decompose_rules: Vec<DecomposeRule> = if should_parse_decompose_rules(strategy) {
282 opts.split_tags
283 .as_ref()
284 .map(|s| parse_decompose_spec(s))
285 .unwrap_or_default()
286 } else {
287 Vec::new()
288 };
289 let decompose_rules_ref = if decompose_rules.is_empty() {
290 None
291 } else {
292 Some(decompose_rules.as_slice())
293 };
294 let cwd = std::env::current_dir().unwrap_or_else(|_| std::path::Path::new(".").to_path_buf());
295 let resolved_ignore = crate::ignore_file::resolve_xml_ignore_path(opts.ignore_path, &cwd);
296 let multi_level_rules_ref = if multi_level_rules.is_empty() {
297 None
298 } else {
299 Some(multi_level_rules.as_slice())
300 };
301 let mut handler = DisassembleXmlFileHandler::new();
302 handler
303 .disassemble(
304 path,
305 opts.unique_id_elements,
306 Some(strategy),
307 opts.pre_purge,
308 opts.post_purge,
309 &resolved_ignore,
310 opts.format,
311 multi_level_rules_ref,
312 decompose_rules_ref,
313 )
314 .await?;
315 Ok(())
316}
317
318async fn run_reassemble(args: &[String]) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
319 let (path, extension, post_purge) = parse_reassemble_args(args);
320 let path = path.unwrap_or(".");
321 let handler = ReassembleXmlFileHandler::new();
322 handler
323 .reassemble(path, extension.or(Some("xml")), post_purge)
324 .await?;
325 Ok(())
326}
327
328#[cfg(test)]
329mod tests {
330 use super::*;
331
332 fn sv(s: &str) -> String {
333 s.to_string()
334 }
335
336 #[test]
337 fn parse_decompose_spec_three_segments_defaults_path_segment_to_tag() {
338 let rules = parse_decompose_spec("objectPermissions:split:object");
339 assert_eq!(rules.len(), 1);
340 let r = &rules[0];
341 assert_eq!(r.tag, "objectPermissions");
342 assert_eq!(r.path_segment, "objectPermissions");
343 assert_eq!(r.mode, "split");
344 assert_eq!(r.field, "object");
345 }
346
347 #[test]
348 fn parse_decompose_spec_four_segments_uses_explicit_path_segment() {
349 let rules = parse_decompose_spec("fieldPermissions:fieldPerms:group:field");
350 assert_eq!(rules.len(), 1);
351 let r = &rules[0];
352 assert_eq!(r.tag, "fieldPermissions");
353 assert_eq!(r.path_segment, "fieldPerms");
354 assert_eq!(r.mode, "group");
355 assert_eq!(r.field, "field");
356 }
357
358 #[test]
359 fn parse_decompose_spec_comma_separated_rules_trims_whitespace() {
360 let rules = parse_decompose_spec("a:split:f, b:group:g , c:x:split:y");
361 assert_eq!(rules.len(), 3);
362 assert_eq!(rules[0].tag, "a");
363 assert_eq!(rules[1].tag, "b");
364 assert_eq!(rules[2].tag, "c");
365 assert_eq!(rules[2].path_segment, "x");
366 }
367
368 #[test]
369 fn parse_decompose_spec_rejects_empty_segments() {
370 assert!(parse_decompose_spec("only:two").is_empty());
372 assert!(parse_decompose_spec(":split:field").is_empty());
374 assert!(parse_decompose_spec("tag::field").is_empty());
375 assert!(parse_decompose_spec("tag:split:").is_empty());
376 }
377
378 #[test]
379 fn parse_multi_level_spec_valid_returns_rule() {
380 let rule = parse_multi_level_spec(
381 "programProcesses-meta:LoyaltyProgramSetup:parameterName,ruleName",
382 )
383 .unwrap();
384 assert_eq!(rule.file_pattern, "programProcesses-meta");
385 assert_eq!(rule.root_to_strip, "LoyaltyProgramSetup");
386 assert_eq!(rule.unique_id_elements, "parameterName,ruleName");
387 assert_eq!(rule.path_segment, "programProcesses");
388 assert_eq!(rule.wrap_root_element, "LoyaltyProgramSetup");
389 assert!(rule.wrap_xmlns.is_empty());
390 }
391
392 #[test]
393 fn parse_multi_level_spec_rejects_wrong_parts() {
394 assert!(parse_multi_level_spec("only:two").is_none());
395 assert!(parse_multi_level_spec(":Root:ids").is_none());
396 assert!(parse_multi_level_spec("file::ids").is_none());
397 assert!(parse_multi_level_spec("file:Root:").is_none());
398 }
399
400 #[test]
401 fn parse_multi_level_specs_single_rule_returns_one() {
402 let rules = parse_multi_level_specs("a-meta:Root:id");
403 assert_eq!(rules.len(), 1);
404 assert_eq!(rules[0].file_pattern, "a-meta");
405 }
406
407 #[test]
408 fn parse_multi_level_specs_semicolon_separates_rules() {
409 let rules =
410 parse_multi_level_specs("a-meta:RootA:id1,id2; b-meta:RootB:other ; c-meta:RootC:k");
411 assert_eq!(rules.len(), 3);
412 assert_eq!(rules[0].file_pattern, "a-meta");
413 assert_eq!(rules[0].unique_id_elements, "id1,id2");
414 assert_eq!(rules[1].file_pattern, "b-meta");
415 assert_eq!(rules[1].root_to_strip, "RootB");
416 assert_eq!(rules[2].file_pattern, "c-meta");
417 }
418
419 #[test]
420 fn parse_multi_level_specs_skips_empty_and_malformed() {
421 let rules = parse_multi_level_specs("a:R:id; ; bad ; b:R:id;");
423 assert_eq!(rules.len(), 2);
424 assert_eq!(rules[0].file_pattern, "a");
425 assert_eq!(rules[1].file_pattern, "b");
426 }
427
428 #[test]
429 fn parse_multi_level_specs_empty_string_returns_empty() {
430 assert!(parse_multi_level_specs("").is_empty());
431 assert!(parse_multi_level_specs(" ; ;").is_empty());
432 }
433
434 #[test]
435 fn parse_disassemble_args_handles_flags_and_eq_forms() {
436 let args = [
437 "path/to/file.xml",
438 "--postpurge",
439 "--prepurge",
440 "--unique-id-elements=name,id",
441 "--ignore-path=.foo",
442 "--format=json",
443 "--strategy=grouped-by-tag",
444 "--multi-level=pattern:Root:ids",
445 "--split-tags=a:split:b",
446 ]
447 .iter()
448 .map(|s| sv(s))
449 .collect::<Vec<_>>();
450 let opts = parse_disassemble_args(&args);
451 assert_eq!(opts.path, Some("path/to/file.xml"));
452 assert!(opts.pre_purge);
453 assert!(opts.post_purge);
454 assert_eq!(opts.unique_id_elements, Some("name,id"));
455 assert_eq!(opts.ignore_path, Some(".foo"));
456 assert_eq!(opts.format, "json");
457 assert_eq!(opts.strategy, Some("grouped-by-tag"));
458 assert_eq!(opts.multi_level.as_deref(), Some("pattern:Root:ids"));
459 assert_eq!(opts.split_tags.as_deref(), Some("a:split:b"));
460 }
461
462 #[test]
463 fn parse_disassemble_args_handles_space_separated_forms() {
464 let args = [
465 "file.xml",
466 "--unique-id-elements",
467 "name",
468 "--ignore-path",
469 ".gitignore",
470 "--format",
471 "yaml",
472 "--strategy",
473 "unique-id",
474 "--multi-level",
475 "p:R:ids",
476 "--split-tags",
477 "t:split:f",
478 ]
479 .iter()
480 .map(|s| sv(s))
481 .collect::<Vec<_>>();
482 let opts = parse_disassemble_args(&args);
483 assert_eq!(opts.path, Some("file.xml"));
484 assert_eq!(opts.unique_id_elements, Some("name"));
485 assert_eq!(opts.ignore_path, Some(".gitignore"));
486 assert_eq!(opts.format, "yaml");
487 assert_eq!(opts.strategy, Some("unique-id"));
488 assert_eq!(opts.multi_level.as_deref(), Some("p:R:ids"));
489 assert_eq!(opts.split_tags.as_deref(), Some("t:split:f"));
490 }
491
492 #[test]
493 fn parse_disassemble_args_space_form_value_is_not_misread_as_positional_path() {
494 let cases: &[(&[&str], &str)] = &[
501 (&["--unique-id-elements", "name"], "name"),
502 (&["--ignore-path", ".foo"], ".foo"),
503 (&["--format", "yaml"], "yaml"),
504 (&["--strategy", "grouped-by-tag"], "grouped-by-tag"),
505 (&["--multi-level", "p:R:ids"], "p:R:ids"),
506 (&["--split-tags", "t:split:f"], "t:split:f"),
507 (&["-p", "t:split:f"], "t:split:f"),
508 ];
509 for (args, expected_value) in cases {
510 let owned: Vec<String> = args.iter().map(|s| sv(s)).collect();
511 let opts = parse_disassemble_args(&owned);
512 assert!(
513 opts.path.is_none(),
514 "args {args:?}: value `{expected_value}` was incorrectly captured as path"
515 );
516 }
517 }
518
519 #[test]
520 fn parse_disassemble_args_space_form_value_missing_does_not_panic() {
521 let options = [
526 "--unique-id-elements",
527 "--ignore-path",
528 "--format",
529 "--strategy",
530 "--multi-level",
531 "--split-tags",
532 "-p",
533 ];
534 for opt in options {
535 let args = [opt].iter().map(|s| sv(s)).collect::<Vec<_>>();
536 let opts = parse_disassemble_args(&args);
537 assert!(opts.path.is_none(), "bare option `{opt}` set a path");
540 }
541 }
542
543 #[test]
544 fn parse_disassemble_args_p_alias_for_split_tags() {
545 let args = ["file.xml", "-p", "a:split:b"]
546 .iter()
547 .map(|s| sv(s))
548 .collect::<Vec<_>>();
549 let opts = parse_disassemble_args(&args);
550 assert_eq!(opts.split_tags.as_deref(), Some("a:split:b"));
551 }
552
553 #[test]
554 fn parse_disassemble_args_unknown_long_flag_is_skipped() {
555 let args = ["file.xml", "--unknown"]
556 .iter()
557 .map(|s| sv(s))
558 .collect::<Vec<_>>();
559 let opts = parse_disassemble_args(&args);
560 assert_eq!(opts.path, Some("file.xml"));
561 }
562
563 #[test]
564 fn parse_disassemble_args_defaults_when_empty() {
565 let opts = parse_disassemble_args(&[]);
566 assert!(opts.path.is_none());
567 assert!(opts.strategy.is_none());
568 assert!(opts.unique_id_elements.is_none());
569 assert!(!opts.pre_purge);
570 assert!(!opts.post_purge);
571 assert!(
572 opts.ignore_path.is_none(),
573 "default is `None` so the runner can pick the right filename"
574 );
575 assert_eq!(opts.format, "xml");
576 }
577
578 #[test]
579 fn parse_disassemble_args_space_forms_without_value_leave_default() {
580 let args = ["--unique-id-elements"]
581 .iter()
582 .map(|s| sv(s))
583 .collect::<Vec<_>>();
584 let opts = parse_disassemble_args(&args);
585 assert!(opts.unique_id_elements.is_none());
586 }
587
588 #[test]
589 fn parse_disassemble_args_trailing_extra_positional_ignored() {
590 let args = ["first.xml", "second.xml"]
591 .iter()
592 .map(|s| sv(s))
593 .collect::<Vec<_>>();
594 let opts = parse_disassemble_args(&args);
595 assert_eq!(opts.path, Some("first.xml"));
596 }
597
598 #[test]
599 fn parse_reassemble_args_picks_path_extension_and_flag() {
600 let args = ["some/dir", "json", "--postpurge"]
601 .iter()
602 .map(|s| sv(s))
603 .collect::<Vec<_>>();
604 let (path, ext, purge) = parse_reassemble_args(&args);
605 assert_eq!(path, Some("some/dir"));
606 assert_eq!(ext, Some("json"));
607 assert!(purge);
608 }
609
610 #[test]
611 fn parse_reassemble_args_defaults_and_extra_args_ignored() {
612 let (p, e, purge) = parse_reassemble_args(&[]);
613 assert!(p.is_none());
614 assert!(e.is_none());
615 assert!(!purge);
616
617 let args = ["dir", "xml", "extra"]
618 .iter()
619 .map(|s| sv(s))
620 .collect::<Vec<_>>();
621 let (p, e, _) = parse_reassemble_args(&args);
622 assert_eq!(p, Some("dir"));
623 assert_eq!(e, Some("xml"));
624 }
625
626 #[tokio::test]
627 async fn run_no_args_prints_usage_and_succeeds() {
628 run(vec![sv("xml-disassembler")]).await.unwrap();
629 }
630
631 #[tokio::test]
632 async fn run_unknown_command_is_not_an_error() {
633 run(vec![sv("xml-disassembler"), sv("unknown")])
634 .await
635 .unwrap();
636 }
637
638 #[tokio::test]
639 async fn run_reassemble_missing_path_returns_err() {
640 let err = run(vec![
642 sv("xml-disassembler"),
643 sv("reassemble"),
644 sv("/definitely/not/here/xyz"),
645 ])
646 .await;
647 assert!(err.is_err());
648 }
649
650 #[tokio::test]
651 async fn run_disassemble_writes_expected_output() {
652 let dir = tempfile::tempdir().unwrap();
653 let xml_path = dir.path().join("sample.xml");
654 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
655<Root xmlns="http://example.com">
656 <child><name>one</name></child>
657 <child><name>two</name></child>
658</Root>"#;
659 std::fs::write(&xml_path, xml).unwrap();
660 run(vec![
661 sv("xml-disassembler"),
662 sv("disassemble"),
663 xml_path.to_string_lossy().to_string(),
664 ])
665 .await
666 .unwrap();
667 assert!(dir.path().join("sample").exists());
668 }
669
670 #[tokio::test]
671 async fn run_disassemble_with_invalid_multi_level_spec_warns_and_continues() {
672 let dir = tempfile::tempdir().unwrap();
673 let xml_path = dir.path().join("sample.xml");
674 let xml =
675 r#"<?xml version="1.0" encoding="UTF-8"?><Root><child><name>a</name></child></Root>"#;
676 std::fs::write(&xml_path, xml).unwrap();
677 run(vec![
678 sv("xml-disassembler"),
679 sv("disassemble"),
680 xml_path.to_string_lossy().to_string(),
681 sv("--multi-level=bad-spec"),
682 ])
683 .await
684 .unwrap();
685 }
686
687 #[tokio::test]
688 async fn run_reassemble_on_existing_directory_succeeds() {
689 let dir = tempfile::tempdir().unwrap();
691 let xml_path = dir.path().join("reasm.xml");
692 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
693<Root><child><name>one</name></child><child><name>two</name></child></Root>"#;
694 std::fs::write(&xml_path, xml).unwrap();
695 run(vec![
696 sv("xml-disassembler"),
697 sv("disassemble"),
698 xml_path.to_string_lossy().to_string(),
699 ])
700 .await
701 .unwrap();
702 let disassembled_dir = dir.path().join("reasm");
703 assert!(disassembled_dir.exists());
704 run(vec![
705 sv("xml-disassembler"),
706 sv("reassemble"),
707 disassembled_dir.to_string_lossy().to_string(),
708 ])
709 .await
710 .unwrap();
711 }
712
713 #[tokio::test]
714 async fn run_disassemble_with_grouped_by_tag_split_tags_runs() {
715 let dir = tempfile::tempdir().unwrap();
716 let xml_path = dir.path().join("perms.xml");
717 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
718<Root>
719 <objectPermissions><object>A</object><allowRead>true</allowRead></objectPermissions>
720 <objectPermissions><object>B</object><allowRead>false</allowRead></objectPermissions>
721</Root>"#;
722 std::fs::write(&xml_path, xml).unwrap();
723 run(vec![
724 sv("xml-disassembler"),
725 sv("disassemble"),
726 xml_path.to_string_lossy().to_string(),
727 sv("--strategy=grouped-by-tag"),
728 sv("-p"),
729 sv("objectPermissions:split:object"),
730 ])
731 .await
732 .unwrap();
733 }
734
735 #[test]
736 fn should_print_usage_only_for_fewer_than_two_args() {
737 assert!(should_print_usage(0));
740 assert!(should_print_usage(1));
741 assert!(!should_print_usage(2));
742 assert!(!should_print_usage(3));
743 }
744
745 #[test]
746 fn multi_level_spec_failed_to_parse_requires_both_conditions() {
747 assert!(multi_level_spec_failed_to_parse(true, true));
750 assert!(!multi_level_spec_failed_to_parse(true, false));
751 assert!(!multi_level_spec_failed_to_parse(false, true));
752 assert!(!multi_level_spec_failed_to_parse(false, false));
753 }
754
755 #[test]
756 fn should_parse_decompose_rules_only_for_grouped_by_tag() {
757 assert!(should_parse_decompose_rules("grouped-by-tag"));
763 assert!(!should_parse_decompose_rules("unique-id"));
764 assert!(!should_parse_decompose_rules(""));
765 assert!(!should_parse_decompose_rules("Grouped-By-Tag"));
766 }
767}