gdtest_analysis/
mutate.rs1#[derive(Debug, Clone)]
9pub struct Mutation {
10 pub start: usize,
12 pub end: usize,
13 pub from: &'static str,
15 pub to: &'static str,
17 pub line: u32,
19}
20
21impl Mutation {
22 pub fn apply(&self, source: &[u8]) -> Vec<u8> {
24 let mut out = Vec::with_capacity(source.len() + self.to.len());
25 out.extend_from_slice(&source[..self.start]);
26 out.extend_from_slice(self.to.as_bytes());
27 out.extend_from_slice(&source[self.end..]);
28 out
29 }
30
31 pub fn description(&self) -> String {
33 format!("line {}: `{}` → `{}`", self.line, self.from, self.to)
34 }
35}
36
37static OPERATORS: &[(&str, &str)] = &[
40 (">=", ">"),
41 ("<=", "<"),
42 ("!=", "=="),
43 ("==", "!="),
44 (" and ", " or "),
45 (" or ", " and "),
46 (" + ", " - "),
47 (" - ", " + "),
48 (" * ", " / "),
49 (" / ", " * "),
50 (">", ">="),
51 ("<", "<="),
52 ("true", "false"),
53 ("false", "true"),
54];
55
56pub fn find_mutations(source: &[u8]) -> Vec<Mutation> {
58 let text = match std::str::from_utf8(source) {
59 Ok(s) => s,
60 Err(_) => return Vec::new(),
61 };
62
63 let mut mutations: Vec<Mutation> = Vec::new();
64 let mut byte_offset: usize = 0;
65
66 for (line_idx, raw_line) in text.lines().enumerate() {
67 let line_num = (line_idx + 1) as u32;
68
69 let line = raw_line.trim_end_matches('\r');
71
72 let code_end = comment_start(line);
74 let code = &line[..code_end];
75
76 let mut used: Vec<(usize, usize)> = Vec::new();
79
80 for &(from, to) in OPERATORS {
81 let mut scan = 0usize;
82 while scan < code.len() {
83 let Some(rel) = code[scan..].find(from) else {
84 break;
85 };
86 let abs = scan + rel;
87 scan = abs + 1; if used.iter().any(|&(s, e)| s < abs + from.len() && e > abs) {
91 continue;
92 }
93
94 if inside_string(code, abs) {
96 continue;
97 }
98
99 if from == ">" || from == "<" {
101 let prev = abs.checked_sub(1).map(|i| code.as_bytes()[i]);
102 let next = code.as_bytes().get(abs + from.len()).copied();
103 if matches!(prev, Some(b'>' | b'<' | b'=' | b'!')) || matches!(next, Some(b'='))
104 {
105 continue;
106 }
107 }
108
109 if from == "true" || from == "false" {
111 let prev = abs.checked_sub(1).map(|i| code.as_bytes()[i]);
112 let next = code.as_bytes().get(abs + from.len()).copied();
113 let prev_ok = prev.is_none_or(|b| !b.is_ascii_alphanumeric() && b != b'_');
114 let next_ok = next.is_none_or(|b| !b.is_ascii_alphanumeric() && b != b'_');
115 if !prev_ok || !next_ok {
116 continue;
117 }
118 }
119
120 used.push((abs, abs + from.len()));
121 mutations.push(Mutation {
122 start: byte_offset + abs,
123 end: byte_offset + abs + from.len(),
124 from,
125 to,
126 line: line_num,
127 });
128 scan = abs + from.len();
129 }
130 }
131
132 byte_offset += raw_line.len() + 1; }
134
135 mutations
136}
137
138fn comment_start(line: &str) -> usize {
141 let mut in_str: Option<char> = None;
142 for (i, c) in line.char_indices() {
143 match in_str {
144 Some(q) if c == q => in_str = None,
145 None if c == '"' || c == '\'' => in_str = Some(c),
146 None if c == '#' => return i,
147 _ => {}
148 }
149 }
150 line.len()
151}
152
153fn inside_string(line: &str, pos: usize) -> bool {
155 let mut in_str: Option<char> = None;
156 for (i, c) in line.char_indices() {
157 if i >= pos {
158 break;
159 }
160 match in_str {
161 Some(q) if c == q => in_str = None,
162 None if c == '"' || c == '\'' => in_str = Some(c),
163 _ => {}
164 }
165 }
166 in_str.is_some()
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172
173 #[test]
174 fn finds_comparison_mutations() {
175 let src = b"if health > 0:\n pass\n";
176 let muts = find_mutations(src);
177 assert!(
178 muts.iter()
179 .any(|m| m.from == ">" && m.to == ">=" && m.line == 1),
180 "expected > → >= mutation: {muts:?}"
181 );
182 }
183
184 #[test]
185 fn skips_greater_equal_for_plain_greater() {
186 let src = b"if x >= 0:\n pass\n";
187 let muts = find_mutations(src);
188 let gt_to_gte: Vec<_> = muts
190 .iter()
191 .filter(|m| m.from == ">" && m.to == ">=")
192 .collect();
193 assert!(
194 gt_to_gte.is_empty(),
195 "bare > should not appear inside >=: {gt_to_gte:?}"
196 );
197 }
198
199 #[test]
200 fn skips_operators_in_strings() {
201 let src = b"var s = \"a > b\"\nif x > 0:\n pass\n";
202 let muts = find_mutations(src);
203 let line1: Vec<_> = muts.iter().filter(|m| m.line == 1).collect();
205 assert!(line1.is_empty(), "no mutations inside strings: {line1:?}");
206 assert!(muts.iter().any(|m| m.line == 2 && m.from == ">"));
207 }
208
209 #[test]
210 fn skips_comments() {
211 let src = b"# if x > 0: pass\nif y > 0:\n pass\n";
212 let muts = find_mutations(src);
213 let line1: Vec<_> = muts.iter().filter(|m| m.line == 1).collect();
214 assert!(line1.is_empty(), "no mutations in comments: {line1:?}");
215 }
216
217 #[test]
218 fn true_false_mutation_respects_word_boundary() {
219 let src = b"var is_true := true\nvar retrue := 1\n";
221 let muts = find_mutations(src);
222 let true_muts: Vec<_> = muts
223 .iter()
224 .filter(|m| m.from == "true" && m.to == "false")
225 .collect();
226 assert_eq!(
227 true_muts.len(),
228 1,
229 "expected exactly 1 true→false: {true_muts:?}"
230 );
231 assert_eq!(true_muts[0].line, 1);
233 let col = true_muts[0].start;
234 assert!(
235 col > 10,
236 "mutation should be past the identifier, got byte offset {col}"
237 );
238 }
239
240 #[test]
241 fn apply_mutation_replaces_bytes() {
242 let src = b"if x > 0:\n pass\n";
243 let muts = find_mutations(src);
244 let gt_mut = muts.iter().find(|m| m.from == ">").unwrap();
245 let mutated = gt_mut.apply(src);
246 let mutated_str = std::str::from_utf8(&mutated).unwrap();
247 assert!(
248 mutated_str.contains(">="),
249 "mutation not applied: {mutated_str}"
250 );
251 assert!(
252 !mutated_str.contains(" > ") || mutated_str.contains(">="),
253 "original not replaced"
254 );
255 }
256}