1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
//! The scenario corpus: every directory under tests/corpus/scenarios/
//! is one merge case. Adding a scenario is adding files, no code.
//!
//! A scenario holds O.<ext>, A.<ext>, B.<ext>, and either
//! expected.<ext> (must merge cleanly and match structurally) or
//! expected.CONFLICT (must conflict; the file names the rule).
use std::path::{Path, PathBuf};
use d3j::diff::Shape;
use d3j::merge::{MergeResult, merge_to_text};
use d3j::{Lang, Tree};
#[test]
fn scenarios() {
let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/corpus/scenarios");
let mut ran = 0usize;
let entries = fs_err::read_dir(&root).expect("scenario corpus directory exists");
for entry in entries {
let dir = entry.expect("readable scenario entry").path();
if !dir.is_dir() {
continue;
}
run_scenario(&dir);
ran = ran.saturating_add(1);
}
assert!(ran > 0, "the scenario corpus must not be empty");
}
fn run_scenario(dir: &Path) {
let name = dir.file_name().and_then(|n| n.to_str()).unwrap_or("?");
let o_path = input(dir, "O").unwrap_or_else(|| panic!("{name}: missing O.<ext>"));
let lang = Lang::detect(&o_path).unwrap_or_else(|| panic!("{name}: unknown language"));
let parse = |stem: &str| -> Tree {
let path = input(dir, stem).unwrap_or_else(|| panic!("{name}: missing {stem}.<ext>"));
let source =
fs_err::read_to_string(&path).unwrap_or_else(|e| panic!("{name}: {stem}: {e}"));
Tree::parse(&source, lang).unwrap_or_else(|e| panic!("{name}: {stem} does not parse: {e}"))
};
let o = parse("O");
let a = parse("A");
let b = parse("B");
let outcome = merge_to_text(&o, &a, &b).unwrap_or_else(|e| panic!("{name}: merge failed: {e}"));
let conflict_marker = dir.join("expected.CONFLICT");
if conflict_marker.exists() {
let rule = fs_err::read_to_string(&conflict_marker)
.unwrap_or_else(|e| panic!("{name}: expected.CONFLICT: {e}"));
let rule = rule.trim();
match outcome {
MergeResult::Conflicts(conflicts) => {
assert!(
conflicts.iter().any(|c| c.rule == rule),
"{name}: expected a {rule} conflict, got {conflicts:?}"
);
}
MergeResult::Merged(text) => {
panic!("{name}: expected a {rule} conflict, merged to {text:?}");
}
}
} else {
let expected = parse("expected");
match outcome {
MergeResult::Merged(text) => {
let merged = Tree::parse(&text, lang)
.unwrap_or_else(|e| panic!("{name}: output does not parse: {e}\n{text}"));
assert_eq!(
Shape::of(&merged),
Shape::of(&expected),
"{name}: merged output {text:?} does not match expected"
);
}
MergeResult::Conflicts(conflicts) => {
panic!("{name}: expected a clean merge, got {conflicts:?}");
}
}
}
}
/// The scenario file with the given stem (any extension except the
/// CONFLICT marker).
fn input(dir: &Path, stem: &str) -> Option<PathBuf> {
fs_err::read_dir(dir).ok()?.find_map(|entry| {
let path = entry.ok()?.path();
let matches = path.file_stem().is_some_and(|s| s == stem)
&& path.extension().is_some_and(|e| e != "CONFLICT");
matches.then_some(path)
})
}