1mod badge;
2mod codeclimate;
3mod compact;
4pub mod grouping;
5mod human;
6mod json;
7mod markdown;
8mod sarif;
9#[cfg(test)]
10mod test_helpers;
11
12use std::path::Path;
13use std::process::ExitCode;
14use std::time::Duration;
15
16use fallow_config::{OutputFormat, RulesConfig, Severity};
17use fallow_core::duplicates::DuplicationReport;
18use fallow_core::results::AnalysisResults;
19use fallow_core::trace::{CloneTrace, DependencyTrace, ExportTrace, FileTrace, PipelineTimings};
20
21pub use grouping::OwnershipResolver;
22
23pub struct ReportContext<'a> {
28 pub root: &'a Path,
29 pub rules: &'a RulesConfig,
30 pub elapsed: Duration,
31 pub quiet: bool,
32 pub explain: bool,
33 pub group_by: Option<OwnershipResolver>,
35}
36
37#[must_use]
39pub fn relative_path<'a>(path: &'a Path, root: &Path) -> &'a Path {
40 path.strip_prefix(root).unwrap_or(path)
41}
42
43#[must_use]
46pub fn split_dir_filename(path: &str) -> (&str, &str) {
47 path.rfind('/')
48 .map_or(("", path), |pos| (&path[..=pos], &path[pos + 1..]))
49}
50
51#[must_use]
53pub const fn plural(n: usize) -> &'static str {
54 if n == 1 { "" } else { "s" }
55}
56
57#[must_use]
62pub fn emit_json(value: &serde_json::Value, kind: &str) -> ExitCode {
63 match serde_json::to_string_pretty(value) {
64 Ok(json) => {
65 println!("{json}");
66 ExitCode::SUCCESS
67 }
68 Err(e) => {
69 eprintln!("Error: failed to serialize {kind} output: {e}");
70 ExitCode::from(2)
71 }
72 }
73}
74
75#[must_use]
81pub fn elide_common_prefix<'a>(base: &str, target: &'a str) -> &'a str {
82 let mut last_sep = 0;
83 for (i, (a, b)) in base.bytes().zip(target.bytes()).enumerate() {
84 if a != b {
85 break;
86 }
87 if a == b'/' {
88 last_sep = i + 1;
89 }
90 }
91 if last_sep > 0 && last_sep <= target.len() {
92 &target[last_sep..]
93 } else {
94 target
95 }
96}
97
98fn relative_uri(path: &Path, root: &Path) -> String {
100 normalize_uri(&relative_path(path, root).display().to_string())
101}
102
103#[must_use]
108pub fn normalize_uri(path_str: &str) -> String {
109 path_str
110 .replace('\\', "/")
111 .replace('[', "%5B")
112 .replace(']', "%5D")
113}
114
115#[derive(Clone, Copy, Debug)]
117pub enum Level {
118 Warn,
119 Info,
120 Error,
121}
122
123#[must_use]
124pub const fn severity_to_level(s: Severity) -> Level {
125 match s {
126 Severity::Error => Level::Error,
127 Severity::Warn => Level::Warn,
128 Severity::Off => Level::Info,
130 }
131}
132
133#[must_use]
139pub fn print_results(
140 results: &AnalysisResults,
141 ctx: &ReportContext<'_>,
142 output: OutputFormat,
143 regression: Option<&crate::regression::RegressionOutcome>,
144) -> ExitCode {
145 if let Some(ref resolver) = ctx.group_by {
147 let groups = grouping::group_analysis_results(results, ctx.root, resolver);
148 return print_grouped_results(&groups, results, ctx, output, resolver);
149 }
150
151 match output {
152 OutputFormat::Human => {
153 human::print_human(results, ctx.root, ctx.rules, ctx.elapsed, ctx.quiet);
154 ExitCode::SUCCESS
155 }
156 OutputFormat::Json => {
157 json::print_json(results, ctx.root, ctx.elapsed, ctx.explain, regression)
158 }
159 OutputFormat::Compact => {
160 compact::print_compact(results, ctx.root);
161 ExitCode::SUCCESS
162 }
163 OutputFormat::Sarif => sarif::print_sarif(results, ctx.root, ctx.rules),
164 OutputFormat::Markdown => {
165 markdown::print_markdown(results, ctx.root);
166 ExitCode::SUCCESS
167 }
168 OutputFormat::CodeClimate => codeclimate::print_codeclimate(results, ctx.root, ctx.rules),
169 OutputFormat::Badge => {
170 eprintln!("Error: badge format is only supported for the health command");
171 ExitCode::from(2)
172 }
173 }
174}
175
176#[must_use]
178fn print_grouped_results(
179 groups: &[grouping::ResultGroup],
180 original: &AnalysisResults,
181 ctx: &ReportContext<'_>,
182 output: OutputFormat,
183 resolver: &OwnershipResolver,
184) -> ExitCode {
185 match output {
186 OutputFormat::Human => {
187 human::print_grouped_human(
188 groups,
189 ctx.root,
190 ctx.rules,
191 ctx.elapsed,
192 ctx.quiet,
193 Some(resolver),
194 );
195 ExitCode::SUCCESS
196 }
197 OutputFormat::Json => json::print_grouped_json(
198 groups,
199 original,
200 ctx.root,
201 ctx.elapsed,
202 ctx.explain,
203 resolver,
204 ),
205 OutputFormat::Compact => {
206 compact::print_grouped_compact(groups, ctx.root);
207 ExitCode::SUCCESS
208 }
209 OutputFormat::Markdown => {
210 markdown::print_grouped_markdown(groups, ctx.root);
211 ExitCode::SUCCESS
212 }
213 OutputFormat::Sarif => {
214 sarif::print_grouped_sarif(original, ctx.root, ctx.rules, resolver);
215 ExitCode::SUCCESS
216 }
217 OutputFormat::CodeClimate => {
218 codeclimate::print_grouped_codeclimate(original, ctx.root, ctx.rules, resolver);
219 ExitCode::SUCCESS
220 }
221 OutputFormat::Badge => {
222 eprintln!("Error: badge format is only supported for the health command");
223 ExitCode::from(2)
224 }
225 }
226}
227
228#[must_use]
232pub fn print_duplication_report(
233 report: &DuplicationReport,
234 ctx: &ReportContext<'_>,
235 output: OutputFormat,
236) -> ExitCode {
237 match output {
238 OutputFormat::Human => {
239 human::print_duplication_human(report, ctx.root, ctx.elapsed, ctx.quiet);
240 ExitCode::SUCCESS
241 }
242 OutputFormat::Json => json::print_duplication_json(report, ctx.elapsed, ctx.explain),
243 OutputFormat::Compact => {
244 compact::print_duplication_compact(report, ctx.root);
245 ExitCode::SUCCESS
246 }
247 OutputFormat::Sarif => sarif::print_duplication_sarif(report, ctx.root),
248 OutputFormat::Markdown => {
249 markdown::print_duplication_markdown(report, ctx.root);
250 ExitCode::SUCCESS
251 }
252 OutputFormat::CodeClimate => codeclimate::print_duplication_codeclimate(report, ctx.root),
253 OutputFormat::Badge => {
254 eprintln!("Error: badge format is only supported for the health command");
255 ExitCode::from(2)
256 }
257 }
258}
259
260#[must_use]
264pub fn print_health_report(
265 report: &crate::health_types::HealthReport,
266 ctx: &ReportContext<'_>,
267 output: OutputFormat,
268) -> ExitCode {
269 match output {
270 OutputFormat::Human => {
271 human::print_health_human(report, ctx.root, ctx.elapsed, ctx.quiet);
272 ExitCode::SUCCESS
273 }
274 OutputFormat::Compact => {
275 compact::print_health_compact(report, ctx.root);
276 ExitCode::SUCCESS
277 }
278 OutputFormat::Markdown => {
279 markdown::print_health_markdown(report, ctx.root);
280 ExitCode::SUCCESS
281 }
282 OutputFormat::Sarif => sarif::print_health_sarif(report, ctx.root),
283 OutputFormat::Json => json::print_health_json(report, ctx.root, ctx.elapsed, ctx.explain),
284 OutputFormat::CodeClimate => codeclimate::print_health_codeclimate(report, ctx.root),
285 OutputFormat::Badge => badge::print_health_badge(report),
286 }
287}
288
289pub fn print_cross_reference_findings(
293 cross_ref: &fallow_core::cross_reference::CrossReferenceResult,
294 root: &Path,
295 quiet: bool,
296 output: OutputFormat,
297) {
298 human::print_cross_reference_findings(cross_ref, root, quiet, output);
299}
300
301pub fn print_export_trace(trace: &ExportTrace, format: OutputFormat) {
305 match format {
306 OutputFormat::Json => json::print_trace_json(trace),
307 _ => human::print_export_trace_human(trace),
308 }
309}
310
311pub fn print_file_trace(trace: &FileTrace, format: OutputFormat) {
313 match format {
314 OutputFormat::Json => json::print_trace_json(trace),
315 _ => human::print_file_trace_human(trace),
316 }
317}
318
319pub fn print_dependency_trace(trace: &DependencyTrace, format: OutputFormat) {
321 match format {
322 OutputFormat::Json => json::print_trace_json(trace),
323 _ => human::print_dependency_trace_human(trace),
324 }
325}
326
327pub fn print_clone_trace(trace: &CloneTrace, root: &Path, format: OutputFormat) {
329 match format {
330 OutputFormat::Json => json::print_trace_json(trace),
331 _ => human::print_clone_trace_human(trace, root),
332 }
333}
334
335pub fn print_performance(timings: &PipelineTimings, format: OutputFormat) {
338 match format {
339 OutputFormat::Json => match serde_json::to_string_pretty(timings) {
340 Ok(json) => eprintln!("{json}"),
341 Err(e) => eprintln!("Error: failed to serialize timings: {e}"),
342 },
343 _ => human::print_performance_human(timings),
344 }
345}
346
347#[allow(
350 unused_imports,
351 reason = "target-dependent: used in lib, unused in bin"
352)]
353pub use codeclimate::build_codeclimate;
354#[allow(
355 unused_imports,
356 reason = "target-dependent: used in lib, unused in bin"
357)]
358pub use codeclimate::build_duplication_codeclimate;
359#[allow(
360 unused_imports,
361 reason = "target-dependent: used in lib, unused in bin"
362)]
363pub use codeclimate::build_health_codeclimate;
364#[allow(
365 unused_imports,
366 reason = "target-dependent: used in lib, unused in bin"
367)]
368pub use compact::build_compact_lines;
369#[allow(
370 unused_imports,
371 reason = "target-dependent: used in lib, unused in bin"
372)]
373pub use json::build_json;
374#[allow(
375 unused_imports,
376 reason = "target-dependent: used in bin audit.rs, unused in lib"
377)]
378#[allow(
379 clippy::redundant_pub_crate,
380 reason = "pub(crate) deliberately limits visibility — report is pub but these are internal"
381)]
382pub(crate) use json::inject_dupes_actions;
383#[allow(
384 unused_imports,
385 reason = "target-dependent: used in bin audit.rs, unused in lib"
386)]
387#[allow(
388 clippy::redundant_pub_crate,
389 reason = "pub(crate) deliberately limits visibility — report is pub but these are internal"
390)]
391pub(crate) use json::inject_health_actions;
392#[allow(
393 unused_imports,
394 reason = "target-dependent: used in lib, unused in bin"
395)]
396pub use markdown::build_duplication_markdown;
397#[allow(
398 unused_imports,
399 reason = "target-dependent: used in lib, unused in bin"
400)]
401pub use markdown::build_health_markdown;
402#[allow(
403 unused_imports,
404 reason = "target-dependent: used in lib, unused in bin"
405)]
406pub use markdown::build_markdown;
407#[allow(
408 unused_imports,
409 reason = "target-dependent: used in lib, unused in bin"
410)]
411pub use sarif::build_health_sarif;
412#[allow(
413 unused_imports,
414 reason = "target-dependent: used in lib, unused in bin"
415)]
416pub use sarif::build_sarif;
417
418#[cfg(test)]
419mod tests {
420 use super::*;
421 use std::path::PathBuf;
422
423 #[test]
426 fn normalize_uri_forward_slashes_unchanged() {
427 assert_eq!(normalize_uri("src/utils.ts"), "src/utils.ts");
428 }
429
430 #[test]
431 fn normalize_uri_backslashes_replaced() {
432 assert_eq!(normalize_uri("src\\utils\\index.ts"), "src/utils/index.ts");
433 }
434
435 #[test]
436 fn normalize_uri_mixed_slashes() {
437 assert_eq!(normalize_uri("src\\utils/index.ts"), "src/utils/index.ts");
438 }
439
440 #[test]
441 fn normalize_uri_path_with_spaces() {
442 assert_eq!(
443 normalize_uri("src\\my folder\\file.ts"),
444 "src/my folder/file.ts"
445 );
446 }
447
448 #[test]
449 fn normalize_uri_empty_string() {
450 assert_eq!(normalize_uri(""), "");
451 }
452
453 #[test]
456 fn relative_path_strips_root_prefix() {
457 let root = Path::new("/project");
458 let path = Path::new("/project/src/utils.ts");
459 assert_eq!(relative_path(path, root), Path::new("src/utils.ts"));
460 }
461
462 #[test]
463 fn relative_path_returns_full_path_when_no_prefix() {
464 let root = Path::new("/other");
465 let path = Path::new("/project/src/utils.ts");
466 assert_eq!(relative_path(path, root), path);
467 }
468
469 #[test]
470 fn relative_path_at_root_returns_empty_or_file() {
471 let root = Path::new("/project");
472 let path = Path::new("/project/file.ts");
473 assert_eq!(relative_path(path, root), Path::new("file.ts"));
474 }
475
476 #[test]
477 fn relative_path_deeply_nested() {
478 let root = Path::new("/project");
479 let path = Path::new("/project/packages/ui/src/components/Button.tsx");
480 assert_eq!(
481 relative_path(path, root),
482 Path::new("packages/ui/src/components/Button.tsx")
483 );
484 }
485
486 #[test]
489 fn relative_uri_produces_forward_slash_path() {
490 let root = PathBuf::from("/project");
491 let path = root.join("src").join("utils.ts");
492 let uri = relative_uri(&path, &root);
493 assert_eq!(uri, "src/utils.ts");
494 }
495
496 #[test]
497 fn relative_uri_encodes_brackets() {
498 let root = PathBuf::from("/project");
499 let path = root.join("src/app/[...slug]/page.tsx");
500 let uri = relative_uri(&path, &root);
501 assert_eq!(uri, "src/app/%5B...slug%5D/page.tsx");
502 }
503
504 #[test]
505 fn relative_uri_encodes_nested_dynamic_routes() {
506 let root = PathBuf::from("/project");
507 let path = root.join("src/app/[slug]/[id]/page.tsx");
508 let uri = relative_uri(&path, &root);
509 assert_eq!(uri, "src/app/%5Bslug%5D/%5Bid%5D/page.tsx");
510 }
511
512 #[test]
513 fn relative_uri_no_common_prefix_returns_full() {
514 let root = PathBuf::from("/other");
515 let path = PathBuf::from("/project/src/utils.ts");
516 let uri = relative_uri(&path, &root);
517 assert!(uri.contains("project"));
518 assert!(uri.contains("utils.ts"));
519 }
520
521 #[test]
524 fn severity_error_maps_to_level_error() {
525 assert!(matches!(severity_to_level(Severity::Error), Level::Error));
526 }
527
528 #[test]
529 fn severity_warn_maps_to_level_warn() {
530 assert!(matches!(severity_to_level(Severity::Warn), Level::Warn));
531 }
532
533 #[test]
534 fn severity_off_maps_to_level_info() {
535 assert!(matches!(severity_to_level(Severity::Off), Level::Info));
536 }
537
538 #[test]
541 fn normalize_uri_single_bracket_pair() {
542 assert_eq!(normalize_uri("app/[id]/page.tsx"), "app/%5Bid%5D/page.tsx");
543 }
544
545 #[test]
546 fn normalize_uri_catch_all_route() {
547 assert_eq!(
548 normalize_uri("app/[...slug]/page.tsx"),
549 "app/%5B...slug%5D/page.tsx"
550 );
551 }
552
553 #[test]
554 fn normalize_uri_optional_catch_all_route() {
555 assert_eq!(
556 normalize_uri("app/[[...slug]]/page.tsx"),
557 "app/%5B%5B...slug%5D%5D/page.tsx"
558 );
559 }
560
561 #[test]
562 fn normalize_uri_multiple_dynamic_segments() {
563 assert_eq!(
564 normalize_uri("app/[lang]/posts/[id]"),
565 "app/%5Blang%5D/posts/%5Bid%5D"
566 );
567 }
568
569 #[test]
570 fn normalize_uri_no_special_chars() {
571 let plain = "src/components/Button.tsx";
572 assert_eq!(normalize_uri(plain), plain);
573 }
574
575 #[test]
576 fn normalize_uri_only_backslashes() {
577 assert_eq!(normalize_uri("a\\b\\c"), "a/b/c");
578 }
579
580 #[test]
583 fn relative_path_identical_paths_returns_empty() {
584 let root = Path::new("/project");
585 assert_eq!(relative_path(root, root), Path::new(""));
586 }
587
588 #[test]
589 fn relative_path_partial_name_match_not_stripped() {
590 let root = Path::new("/project");
593 let path = Path::new("/project-two/src/a.ts");
594 assert_eq!(relative_path(path, root), path);
595 }
596
597 #[test]
600 fn relative_uri_combines_stripping_and_encoding() {
601 let root = PathBuf::from("/project");
602 let path = root.join("src/app/[slug]/page.tsx");
603 let uri = relative_uri(&path, &root);
604 assert_eq!(uri, "src/app/%5Bslug%5D/page.tsx");
606 assert!(!uri.starts_with('/'));
607 }
608
609 #[test]
610 fn relative_uri_at_root_file() {
611 let root = PathBuf::from("/project");
612 let path = root.join("index.ts");
613 assert_eq!(relative_uri(&path, &root), "index.ts");
614 }
615
616 #[test]
619 fn severity_to_level_is_const_evaluable() {
620 const LEVEL_FROM_ERROR: Level = severity_to_level(Severity::Error);
622 const LEVEL_FROM_WARN: Level = severity_to_level(Severity::Warn);
623 const LEVEL_FROM_OFF: Level = severity_to_level(Severity::Off);
624 assert!(matches!(LEVEL_FROM_ERROR, Level::Error));
625 assert!(matches!(LEVEL_FROM_WARN, Level::Warn));
626 assert!(matches!(LEVEL_FROM_OFF, Level::Info));
627 }
628
629 #[test]
632 fn level_is_copy() {
633 let level = severity_to_level(Severity::Error);
634 let copy = level;
635 assert!(matches!(level, Level::Error));
637 assert!(matches!(copy, Level::Error));
638 }
639
640 #[test]
643 fn elide_common_prefix_shared_dir() {
644 assert_eq!(
645 elide_common_prefix("src/components/A.tsx", "src/components/B.tsx"),
646 "B.tsx"
647 );
648 }
649
650 #[test]
651 fn elide_common_prefix_partial_shared() {
652 assert_eq!(
653 elide_common_prefix("src/components/A.tsx", "src/utils/B.tsx"),
654 "utils/B.tsx"
655 );
656 }
657
658 #[test]
659 fn elide_common_prefix_no_shared() {
660 assert_eq!(
661 elide_common_prefix("pkg-a/src/A.tsx", "pkg-b/src/B.tsx"),
662 "pkg-b/src/B.tsx"
663 );
664 }
665
666 #[test]
667 fn elide_common_prefix_identical_files() {
668 assert_eq!(elide_common_prefix("a/b/x.ts", "a/b/y.ts"), "y.ts");
670 }
671
672 #[test]
673 fn elide_common_prefix_no_dirs() {
674 assert_eq!(elide_common_prefix("foo.ts", "bar.ts"), "bar.ts");
675 }
676
677 #[test]
678 fn elide_common_prefix_deep_monorepo() {
679 assert_eq!(
680 elide_common_prefix(
681 "packages/rap/src/rap/components/SearchSelect/SearchSelect.tsx",
682 "packages/rap/src/rap/components/SearchSelect/SearchSelectItem.tsx"
683 ),
684 "SearchSelectItem.tsx"
685 );
686 }
687
688 #[test]
691 fn split_dir_filename_with_dir() {
692 let (dir, file) = split_dir_filename("src/utils/index.ts");
693 assert_eq!(dir, "src/utils/");
694 assert_eq!(file, "index.ts");
695 }
696
697 #[test]
698 fn split_dir_filename_no_dir() {
699 let (dir, file) = split_dir_filename("file.ts");
700 assert_eq!(dir, "");
701 assert_eq!(file, "file.ts");
702 }
703
704 #[test]
705 fn split_dir_filename_deeply_nested() {
706 let (dir, file) = split_dir_filename("a/b/c/d/e.ts");
707 assert_eq!(dir, "a/b/c/d/");
708 assert_eq!(file, "e.ts");
709 }
710
711 #[test]
712 fn split_dir_filename_trailing_slash() {
713 let (dir, file) = split_dir_filename("src/");
714 assert_eq!(dir, "src/");
715 assert_eq!(file, "");
716 }
717
718 #[test]
719 fn split_dir_filename_empty() {
720 let (dir, file) = split_dir_filename("");
721 assert_eq!(dir, "");
722 assert_eq!(file, "");
723 }
724
725 #[test]
728 fn plural_zero_is_plural() {
729 assert_eq!(plural(0), "s");
730 }
731
732 #[test]
733 fn plural_one_is_singular() {
734 assert_eq!(plural(1), "");
735 }
736
737 #[test]
738 fn plural_two_is_plural() {
739 assert_eq!(plural(2), "s");
740 }
741
742 #[test]
743 fn plural_large_number() {
744 assert_eq!(plural(999), "s");
745 }
746
747 #[test]
750 fn elide_common_prefix_empty_base() {
751 assert_eq!(elide_common_prefix("", "src/foo.ts"), "src/foo.ts");
752 }
753
754 #[test]
755 fn elide_common_prefix_empty_target() {
756 assert_eq!(elide_common_prefix("src/foo.ts", ""), "");
757 }
758
759 #[test]
760 fn elide_common_prefix_both_empty() {
761 assert_eq!(elide_common_prefix("", ""), "");
762 }
763
764 #[test]
765 fn elide_common_prefix_same_file_different_extension() {
766 assert_eq!(
768 elide_common_prefix("src/utils.ts", "src/utils.js"),
769 "utils.js"
770 );
771 }
772
773 #[test]
774 fn elide_common_prefix_partial_filename_match_not_stripped() {
775 assert_eq!(
777 elide_common_prefix("src/App.tsx", "src/AppUtils.tsx"),
778 "AppUtils.tsx"
779 );
780 }
781
782 #[test]
783 fn elide_common_prefix_identical_paths() {
784 assert_eq!(elide_common_prefix("src/foo.ts", "src/foo.ts"), "foo.ts");
785 }
786
787 #[test]
788 fn split_dir_filename_single_slash() {
789 let (dir, file) = split_dir_filename("/file.ts");
790 assert_eq!(dir, "/");
791 assert_eq!(file, "file.ts");
792 }
793
794 #[test]
795 fn emit_json_returns_success_for_valid_value() {
796 let value = serde_json::json!({"key": "value"});
797 let code = emit_json(&value, "test");
798 assert_eq!(code, ExitCode::SUCCESS);
799 }
800
801 mod proptests {
802 use super::*;
803 use proptest::prelude::*;
804
805 proptest! {
806 #[test]
808 fn split_dir_filename_reconstructs_path(path in "[a-zA-Z0-9_./\\-]{0,100}") {
809 let (dir, file) = split_dir_filename(&path);
810 let reconstructed = format!("{dir}{file}");
811 prop_assert_eq!(
812 reconstructed, path,
813 "dir+file should reconstruct the original path"
814 );
815 }
816
817 #[test]
819 fn plural_returns_empty_or_s(n: usize) {
820 let result = plural(n);
821 prop_assert!(
822 result.is_empty() || result == "s",
823 "plural should return \"\" or \"s\", got {:?}",
824 result
825 );
826 }
827
828 #[test]
830 fn plural_singular_only_for_one(n: usize) {
831 let result = plural(n);
832 if n == 1 {
833 prop_assert_eq!(result, "", "plural(1) should be empty");
834 } else {
835 prop_assert_eq!(result, "s", "plural({}) should be \"s\"", n);
836 }
837 }
838
839 #[test]
841 fn normalize_uri_no_backslashes(path in "[a-zA-Z0-9_.\\\\/ \\[\\]%-]{0,100}") {
842 let result = normalize_uri(&path);
843 prop_assert!(
844 !result.contains('\\'),
845 "Result should not contain backslashes: {result}"
846 );
847 }
848
849 #[test]
851 fn normalize_uri_encodes_all_brackets(path in "[a-zA-Z0-9_./\\[\\]%-]{0,80}") {
852 let result = normalize_uri(&path);
853 prop_assert!(
854 !result.contains('[') && !result.contains(']'),
855 "Result should not contain raw brackets: {result}"
856 );
857 }
858
859 #[test]
861 fn elide_common_prefix_returns_suffix_of_target(
862 base in "[a-zA-Z0-9_./]{0,50}",
863 target in "[a-zA-Z0-9_./]{0,50}",
864 ) {
865 let result = elide_common_prefix(&base, &target);
866 prop_assert!(
867 target.ends_with(result),
868 "Result {:?} should be a suffix of target {:?}",
869 result, target
870 );
871 }
872
873 #[test]
875 fn relative_path_never_panics(
876 root in "/[a-zA-Z0-9_/]{0,30}",
877 suffix in "[a-zA-Z0-9_./]{0,30}",
878 ) {
879 let root_path = Path::new(&root);
880 let full = PathBuf::from(format!("{root}/{suffix}"));
881 let _ = relative_path(&full, root_path);
882 }
883 }
884 }
885}