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
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
//! The diff round-trip property: for generated (O, A) pairs, the
//! matching validates and the derived edit script rebuilds A's shape.
use std::path::Path;
use std::sync::LazyLock;
use hegel::TestCase;
use hegel::generators::integers;
use d3j::diff::{Shape, apply, diff, edits};
use d3j::{Lang, Tree};
/// The seed corpus: (language, source) pairs from tests/corpus/seeds/.
static SEEDS: LazyLock<Vec<(&'static Lang, String)>> = LazyLock::new(|| {
let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/corpus/seeds");
let mut seeds: Vec<(&'static Lang, String)> = fs_err::read_dir(&dir)
.expect("seed corpus directory exists")
.filter_map(|entry| {
let path = entry.ok()?.path();
let lang = Lang::detect(&path)?;
let source = fs_err::read_to_string(&path).ok()?;
Some((lang, source))
})
.collect();
seeds.sort_by_key(|(lang, _)| lang.name());
assert!(!seeds.is_empty(), "seed corpus must not be empty");
seeds
});
/// Draws an index into 0..len (len must be nonzero).
fn draw_index(tc: &TestCase, len: usize) -> usize {
let max = u32::try_from(len.saturating_sub(1)).unwrap_or(u32::MAX);
tc.draw(integers::<u32>().min_value(0).max_value(max)) as usize
}
/// One small textual edit, or None when the draw picks an inapplicable
/// spot. Callers re-parse to filter out unparsable results.
fn mutate(tc: &TestCase, source: &str, lang: &'static Lang) -> Option<String> {
match tc.draw(integers::<u8>().min_value(0).max_value(2)) {
0 => {
// Duplicate a line.
let lines: Vec<&str> = source.lines().collect();
let i = draw_index(tc, lines.len());
let mut out = lines.clone();
out.insert(i, lines.get(i)?);
Some(out.join("\n"))
}
1 => {
// Delete a line.
let mut lines: Vec<&str> = source.lines().collect();
if lines.len() < 2 {
return None;
}
let i = draw_index(tc, lines.len());
lines.remove(i);
Some(lines.join("\n"))
}
_ => {
// Rewrite one labeled leaf in place.
let tree = Tree::parse(source, lang).ok()?;
let leaves: Vec<_> = tree.nodes().filter(|&n| tree.label(n).is_some()).collect();
if leaves.is_empty() {
return None;
}
let node = *leaves.get(draw_index(tc, leaves.len()))?;
let fresh = tc.draw(integers::<u16>().min_value(0).max_value(999));
let replacement = match tree.kind(node) {
"identifier" | "type_identifier" | "field_identifier" => format!("zz{fresh}"),
"integer_literal" | "number" => format!("{fresh}"),
"string_content" => format!("s{fresh}"),
"true" | "false" => "true".to_string(),
_ => return None,
};
let span = tree.span(node);
let mut out = String::with_capacity(source.len());
out.push_str(source.get(..span.start)?);
out.push_str(&replacement);
out.push_str(source.get(span.end..)?);
Some(out)
}
}
}
/// Applies up to `budget` parsability-preserving edits to `source`.
fn evolve(tc: &TestCase, source: &str, lang: &'static Lang, budget: u8) -> String {
let mut current = source.to_string();
let count = tc.draw(integers::<u8>().min_value(0).max_value(budget));
for _ in 0..count {
if let Some(candidate) = mutate(tc, ¤t, lang)
&& Tree::parse(&candidate, lang).is_ok()
{
current = candidate;
}
}
current
}
#[hegel::test(test_cases = 500)]
fn diff_round_trips_generated_edits(tc: TestCase) {
let (lang, seed) = {
let seeds = &*SEEDS;
let (lang, seed) = &seeds[draw_index(&tc, seeds.len())];
(*lang, seed.clone())
};
let o_source = evolve(&tc, &seed, lang, 2);
let a_source = evolve(&tc, &o_source, lang, 4);
tc.note(&format!("O:\n{o_source}\nA:\n{a_source}"));
let o = Tree::parse(&o_source, lang).expect("O evolved through parsable edits");
let a = Tree::parse(&a_source, lang).expect("A evolved through parsable edits");
let m = diff(&o, &a);
m.validate(&o, &a)
.expect("diff produced an invalid matching");
let script = edits(&o, &a, &m);
assert_eq!(apply(&o, &a, &m, &script), Shape::of(&a));
}