1use std::collections::{BTreeMap, BTreeSet};
12
13use serde::{Deserialize, Serialize};
14use soldb_core::{SoldbError, SoldbResult, TransactionTrace};
15use soldb_ethdebug::{keccak256, parse_word, word_hex};
16
17use crate::{ContractDebugInfo, StepMap};
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
21#[serde(rename_all = "kebab-case")]
22pub enum DebugDiffMode {
23 Steps,
25 Spans,
27 Coverage,
29}
30
31#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
33#[serde(rename_all = "camelCase")]
34pub struct DebugExecution {
35 pub creation: bool,
36 pub input: String,
37 pub value: String,
38 pub success: bool,
39 pub output: String,
40 #[serde(default)]
42 pub constructor_args: Option<String>,
43}
44
45#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
47#[serde(rename_all = "camelCase")]
48pub struct DebugTraceSummary {
49 pub instructions: usize,
50 pub mapped_instructions: usize,
51 pub source_steps: usize,
52 pub frame_entries: usize,
53}
54
55#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
57#[serde(rename_all = "camelCase")]
58pub struct DebugTraceEvent {
59 pub instruction: usize,
61 pub pc: u64,
63 pub contract: String,
64 pub source: String,
65 #[serde(default)]
67 pub source_hash: Option<[u8; 32]>,
68 pub line: u64,
69 pub column: u64,
70 pub offset: u64,
71 pub length: u64,
72 pub function: Option<String>,
73 pub frame_depth: u32,
74 pub frame_entry: bool,
75 pub generated: bool,
76 pub modifier_depth: Option<i64>,
78}
79
80#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
82#[serde(rename_all = "camelCase")]
83pub struct DebugTrace {
84 pub execution: DebugExecution,
85 pub summary: DebugTraceSummary,
86 pub events: Vec<DebugTraceEvent>,
87 #[serde(default)]
89 pub diagnostics: Vec<String>,
90}
91
92#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
94pub struct DebugCheckpoint {
95 pub source: String,
96 pub line: u64,
98}
99
100impl DebugTrace {
101 pub fn retain_checkpoints(&mut self, checkpoints: &[DebugCheckpoint]) {
106 let mut by_line = BTreeMap::<u64, Vec<(usize, String)>>::new();
107 for (index, checkpoint) in checkpoints.iter().enumerate() {
108 by_line
109 .entry(checkpoint.line)
110 .or_default()
111 .push((index, normalize_source_path(&checkpoint.source)));
112 }
113 let mut reached = BTreeSet::new();
114 self.events.retain(|event| {
115 let mut matched = false;
116 for (index, source) in by_line.get(&event.line).into_iter().flatten() {
117 if source_paths_match(&event.source, source) {
118 reached.insert(*index);
119 matched = true;
120 }
121 }
122 matched
123 });
124 for (index, checkpoint) in checkpoints.iter().enumerate() {
125 if !reached.contains(&index) {
126 self.diagnostics.push(format!(
127 "checkpoint {}:{} was not reached",
128 checkpoint.source, checkpoint.line
129 ));
130 }
131 }
132 self.summary.source_steps = self.events.len();
133 }
134
135 pub fn set_constructor_args(&mut self, arguments: &str) -> SoldbResult<()> {
140 let arguments = normalize_hex_data(arguments);
141 let hex = arguments.strip_prefix("0x").unwrap_or(&arguments);
142 if !self.execution.creation
143 || !hex.len().is_multiple_of(2)
144 || !hex.bytes().all(|byte| byte.is_ascii_hexdigit())
145 || !self.execution.input.ends_with(hex)
146 {
147 return Err(SoldbError::Message(
148 "constructor arguments must be hex matching the end of creation input".to_owned(),
149 ));
150 }
151 self.execution.constructor_args = Some(arguments);
152 Ok(())
153 }
154}
155
156#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
158#[serde(rename_all = "kebab-case")]
159pub enum DebugDifferenceKind {
160 Changed,
161 Missing,
162 Unexpected,
163}
164
165#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
167#[serde(rename_all = "camelCase")]
168pub struct DebugDifference {
169 pub kind: DebugDifferenceKind,
170 pub index: usize,
171 #[serde(skip_serializing_if = "Option::is_none")]
172 pub reference: Option<DebugTraceEvent>,
173 #[serde(skip_serializing_if = "Option::is_none")]
174 pub candidate: Option<DebugTraceEvent>,
175}
176
177#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
179#[serde(rename_all = "camelCase")]
180pub struct DebugDiffReport {
181 pub schema_version: u32,
182 pub mode: DebugDiffMode,
183 pub equivalent: bool,
184 pub comparable: bool,
185 pub diagnostics: Vec<String>,
186 pub execution_equivalent: bool,
187 pub execution_differences: Vec<String>,
188 pub reference: DebugTraceSummary,
189 pub candidate: DebugTraceSummary,
190 pub difference_count: usize,
191 pub differences: Vec<DebugDifference>,
192 pub differences_truncated: bool,
193}
194
195#[must_use]
197pub fn capture_debug_trace(
198 trace: &TransactionTrace,
199 contracts: Vec<ContractDebugInfo>,
200) -> DebugTrace {
201 let mut diagnostics = BTreeSet::new();
202 let source_hashes = contracts
203 .iter()
204 .enumerate()
205 .flat_map(|(index, contract)| {
206 contract
207 .source_contents
208 .iter()
209 .map(move |(source, content)| ((index, *source), keccak256(content.as_bytes())))
210 })
211 .collect::<BTreeMap<_, _>>();
212 for contract in &contracts {
213 let mut pcs = BTreeSet::new();
214 for instruction in &contract.info.instructions {
215 if !pcs.insert(instruction.offset) {
216 diagnostics.insert(format!(
217 "duplicate instruction at PC {} in `{}`",
218 instruction.offset, contract.name
219 ));
220 }
221 for source in instruction.source_locations() {
222 let valid = contract
223 .source_contents
224 .get(&source.source_id)
225 .is_some_and(|text| {
226 source
227 .offset
228 .checked_add(source.length)
229 .is_some_and(|end| end <= text.len() as u64)
230 });
231 if !valid {
232 diagnostics.insert(format!(
233 "missing source or invalid source range at PC {} in `{}`",
234 instruction.offset, contract.name
235 ));
236 }
237 }
238 }
239 }
240 let map = StepMap::for_debug_diff(trace, contracts);
241 let mut mapped_instructions = 0;
242 let mut frame_entries = 0;
243 let mut events = Vec::new();
244
245 for instruction in 0..map.step_count() {
246 let step = &trace.steps[instruction];
247 if let Some(contract) = map.contract_at_step(instruction) {
248 let opcode = contract
249 .instruction_at_pc(step.pc)
250 .and_then(|inst| inst.mnemonic());
251 if !opcode.is_some_and(|opcode| opcodes_match(opcode, &step.op)) {
252 diagnostics.insert(format!(
253 "trace opcode `{}` at PC {} disagrees with artifact `{}` in `{}`",
254 step.op,
255 step.pc,
256 opcode.unwrap_or("<missing>"),
257 contract.name
258 ));
259 }
260 } else {
261 diagnostics.insert("trace has steps without a matching contract artifact".to_owned());
262 }
263 if map.line_key(instruction).is_some() {
264 mapped_instructions += 1;
265 }
266 if map.is_frame_entry(instruction) {
267 frame_entries += 1;
268 }
269 if !map.is_line_start(instruction) {
270 continue;
271 }
272
273 let Some(location) = map.location(instruction) else {
274 continue;
275 };
276 let modifier_depth = map
277 .contract_at_step(instruction)
278 .and_then(|contract| contract.modifier_depth_at_pc(step.pc));
279 events.push(DebugTraceEvent {
280 instruction,
281 pc: step.pc,
282 contract: location.contract_name,
283 source: normalize_source_path(&location.path),
284 source_hash: source_hashes
285 .get(&(location.key.contract, location.key.source_id))
286 .copied(),
287 line: location.line,
288 column: location.column,
289 offset: location.offset,
290 length: location.length,
291 function: location.function_name,
292 frame_depth: map.frame_depth(instruction).unwrap_or(0),
293 frame_entry: map.is_frame_entry(instruction),
294 generated: location.generated,
295 modifier_depth,
296 });
297 }
298
299 DebugTrace {
300 execution: DebugExecution {
301 creation: trace.to_addr.is_none(),
302 input: normalize_hex_data(&trace.input_data),
303 value: match parse_word(&trace.value) {
304 Ok(value) => word_hex(&value),
305 Err(error) => {
306 diagnostics.insert(format!("invalid call value: {error}"));
307 trace.value.clone()
308 }
309 },
310 success: trace.success,
311 output: normalize_hex_data(&trace.output),
312 constructor_args: None,
313 },
314 summary: DebugTraceSummary {
315 instructions: map.step_count(),
316 mapped_instructions,
317 source_steps: events.len(),
318 frame_entries,
319 },
320 events,
321 diagnostics: diagnostics.into_iter().collect(),
322 }
323}
324
325#[must_use]
328pub fn compare_debug_traces(
329 reference: &DebugTrace,
330 candidate: &DebugTrace,
331 mode: DebugDiffMode,
332 difference_limit: usize,
333) -> DebugDiffReport {
334 let mut diagnostics = Vec::new();
335 for (side, trace) in [("reference", reference), ("candidate", candidate)] {
336 diagnostics.extend(
337 trace
338 .diagnostics
339 .iter()
340 .map(|message| format!("{side}: {message}")),
341 );
342 if trace.events.iter().any(|event| event.source_hash.is_none()) {
343 diagnostics.push(format!("{side}: source identity is unavailable"));
344 }
345 }
346 if reference.events.is_empty() {
347 diagnostics.push("reference trace has no source steps".to_owned());
348 }
349 if candidate.events.is_empty() {
350 diagnostics.push("candidate trace has no source steps".to_owned());
351 }
352 let execution_differences = compare_execution(&reference.execution, &candidate.execution);
353 let (difference_count, differences) = match mode {
354 DebugDiffMode::Steps | DebugDiffMode::Spans => {
355 compare_ordered_events(&reference.events, &candidate.events, mode, difference_limit)
356 }
357 DebugDiffMode::Coverage => {
358 compare_coverage(&reference.events, &candidate.events, difference_limit)
359 }
360 };
361 let comparable = diagnostics.is_empty();
362 let execution_equivalent = execution_differences.is_empty();
363
364 DebugDiffReport {
365 schema_version: 1,
366 mode,
367 equivalent: comparable && execution_equivalent && difference_count == 0,
368 comparable,
369 diagnostics,
370 execution_equivalent,
371 execution_differences,
372 reference: reference.summary.clone(),
373 candidate: candidate.summary.clone(),
374 difference_count,
375 differences_truncated: difference_count > differences.len(),
376 differences,
377 }
378}
379
380fn compare_execution(reference: &DebugExecution, candidate: &DebugExecution) -> Vec<String> {
381 let mut differences = Vec::new();
382 if reference.creation != candidate.creation {
383 differences.push("execution kind differs".to_owned());
384 }
385 if reference.creation && candidate.creation {
386 match (&reference.constructor_args, &candidate.constructor_args) {
387 (Some(left), Some(right)) if left != right => {
388 differences.push("constructor arguments differ".to_owned());
389 }
390 (Some(_), Some(_)) => {}
391 _ => differences.push("encoded constructor arguments were not supplied".to_owned()),
392 }
393 } else if reference.input != candidate.input {
394 differences.push("calldata differs".to_owned());
395 }
396 if reference.value != candidate.value {
397 differences.push("call value differs".to_owned());
398 }
399 if reference.success != candidate.success {
400 differences.push("success status differs".to_owned());
401 }
402 let deployed =
405 reference.creation && candidate.creation && reference.success && candidate.success;
406 if !deployed && reference.output != candidate.output {
407 differences.push("return data differs".to_owned());
408 }
409 differences
410}
411
412fn compare_ordered_events(
413 reference: &[DebugTraceEvent],
414 candidate: &[DebugTraceEvent],
415 mode: DebugDiffMode,
416 limit: usize,
417) -> (usize, Vec<DebugDifference>) {
418 let mut difference_count = 0;
419 let mut differences = Vec::new();
420 for index in 0..reference.len().max(candidate.len()) {
421 let left = reference.get(index);
422 let right = candidate.get(index);
423 if matches!((left, right), (Some(left), Some(right)) if events_match(left, right, mode)) {
424 continue;
425 }
426
427 difference_count += 1;
428 if differences.len() == limit {
429 continue;
430 }
431 differences.push(DebugDifference {
432 kind: match (left, right) {
433 (Some(_), Some(_)) => DebugDifferenceKind::Changed,
434 (Some(_), None) => DebugDifferenceKind::Missing,
435 (None, Some(_)) => DebugDifferenceKind::Unexpected,
436 (None, None) => continue,
437 },
438 index,
439 reference: left.cloned(),
440 candidate: right.cloned(),
441 });
442 }
443 (difference_count, differences)
444}
445
446fn compare_coverage(
447 reference: &[DebugTraceEvent],
448 candidate: &[DebugTraceEvent],
449 limit: usize,
450) -> (usize, Vec<DebugDifference>) {
451 let reference = unique_coverage(reference);
452 let candidate = unique_coverage(candidate);
453 let mut candidate_by_core = BTreeMap::<CoverageCore<'_>, Vec<usize>>::new();
454 for (index, event) in candidate.iter().enumerate() {
455 candidate_by_core
456 .entry(CoverageCore::new(event))
457 .or_default()
458 .push(index);
459 }
460 let mut matched_candidate = BTreeSet::new();
461 let mut differences = Vec::new();
462 let mut difference_count = 0;
463
464 for (index, left) in reference.iter().enumerate() {
465 let right = candidate_by_core
466 .get(&CoverageCore::new(left))
467 .into_iter()
468 .flatten()
469 .find_map(|candidate_index| {
470 let right = candidate[*candidate_index];
471 (!matched_candidate.contains(candidate_index)
472 && source_paths_match(&left.source, &right.source)
473 && left.source_hash == right.source_hash)
474 .then_some((*candidate_index, right))
475 });
476 if let Some((candidate_index, _)) = right {
477 matched_candidate.insert(candidate_index);
478 continue;
479 }
480 difference_count += 1;
481 if differences.len() < limit {
482 differences.push(DebugDifference {
483 kind: DebugDifferenceKind::Missing,
484 index,
485 reference: Some((*left).clone()),
486 candidate: None,
487 });
488 }
489 }
490
491 for (index, right) in candidate.iter().enumerate() {
492 if matched_candidate.contains(&index) {
493 continue;
494 }
495 difference_count += 1;
496 if differences.len() < limit {
497 differences.push(DebugDifference {
498 kind: DebugDifferenceKind::Unexpected,
499 index,
500 reference: None,
501 candidate: Some((*right).clone()),
502 });
503 }
504 }
505
506 (difference_count, differences)
507}
508
509fn unique_coverage(events: &[DebugTraceEvent]) -> Vec<&DebugTraceEvent> {
510 let mut keys = BTreeSet::new();
511 let mut unique = Vec::new();
512 for event in events {
513 if keys.insert((
514 &event.contract,
515 &event.source,
516 event.source_hash,
517 event.line,
518 &event.function,
519 )) {
520 unique.push(event);
521 }
522 }
523 unique
524}
525
526#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
527struct CoverageCore<'a> {
528 contract: &'a str,
529 line: u64,
530 function: Option<&'a str>,
531}
532
533impl<'a> CoverageCore<'a> {
534 fn new(event: &'a DebugTraceEvent) -> Self {
535 Self {
536 contract: &event.contract,
537 line: event.line,
538 function: event.function.as_deref(),
539 }
540 }
541}
542
543fn events_match(
544 reference: &DebugTraceEvent,
545 candidate: &DebugTraceEvent,
546 mode: DebugDiffMode,
547) -> bool {
548 let source_step = reference.contract == candidate.contract
549 && source_paths_match(&reference.source, &candidate.source)
550 && reference.source_hash == candidate.source_hash
551 && reference.line == candidate.line
552 && reference.function == candidate.function;
553 if mode == DebugDiffMode::Coverage {
554 return source_step;
555 }
556
557 source_step
558 && reference.frame_depth == candidate.frame_depth
559 && reference.frame_entry == candidate.frame_entry
560 && (mode != DebugDiffMode::Spans
561 || (reference.column == candidate.column
562 && reference.offset == candidate.offset
563 && reference.length == candidate.length
564 && reference.generated == candidate.generated
565 && reference.modifier_depth == candidate.modifier_depth))
566}
567
568fn normalize_source_path(path: &str) -> String {
569 let path = path.replace('\\', "/");
570 path.strip_prefix("./").unwrap_or(&path).to_owned()
571}
572
573fn normalize_hex_data(value: &str) -> String {
574 let value = value
575 .strip_prefix("0x")
576 .or_else(|| value.strip_prefix("0X"))
577 .unwrap_or(value);
578 format!("0x{}", value.to_ascii_lowercase())
579}
580
581fn source_paths_match(left: &str, right: &str) -> bool {
582 let absolute = |path: &str| path.starts_with('/') || path.as_bytes().get(1) == Some(&b':');
583 let suffix = |long: &str, short: &str| {
584 long.strip_suffix(short)
585 .is_some_and(|prefix| prefix.ends_with('/'))
586 };
587 left == right
588 || (absolute(left) != absolute(right) && (suffix(left, right) || suffix(right, left)))
589}
590
591fn opcodes_match(left: &str, right: &str) -> bool {
592 let canonical = |opcode| match opcode {
593 "DIFFICULTY" => "PREVRANDAO",
594 "SHA3" => "KECCAK256",
595 other => other,
596 };
597 canonical(left).eq_ignore_ascii_case(canonical(right))
598}
599
600#[cfg(test)]
601mod tests {
602 use std::collections::BTreeMap;
603
604 use serde_json::json;
605 use soldb_core::{TraceStep, TransactionTrace};
606 use soldb_ethdebug::{EthdebugInfo, Instruction};
607
608 use super::{
609 capture_debug_trace, compare_debug_traces, DebugCheckpoint, DebugDiffMode,
610 DebugDifferenceKind,
611 };
612 use crate::ContractDebugInfo;
613
614 const SOURCE: &str = "contract C {\n function f() external {\n uint256 x = 1;\n x++;\n }\n}\n";
615
616 fn trace(pcs: &[u64], output: &str) -> TransactionTrace {
617 TransactionTrace {
618 tx_hash: None,
619 from_addr: "0x1".to_owned(),
620 to_addr: Some("0x2".to_owned()),
621 value: "0x0".to_owned(),
622 input_data: "0x26121ff0".to_owned(),
623 gas_used: 0,
624 output: output.to_owned(),
625 success: true,
626 error: None,
627 debug_trace_available: true,
628 contract_address: None,
629 backend: None,
630 capabilities: Default::default(),
631 artifacts: Default::default(),
632 steps: pcs
633 .iter()
634 .map(|pc| TraceStep {
635 pc: *pc,
636 op: "JUMPDEST".into(),
637 gas: 0,
638 gas_cost: 0,
639 depth: 0,
640 stack: Vec::new(),
641 memory: None,
642 storage: None,
643 error: None,
644 snapshot: Default::default(),
645 })
646 .collect(),
647 }
648 }
649
650 fn contract(pcs: &[u64], offsets: &[usize], path: &str) -> ContractDebugInfo {
651 let instructions = pcs
652 .iter()
653 .zip(offsets)
654 .map(|(pc, offset)| Instruction {
655 offset: *pc,
656 operation: json!({"mnemonic": "JUMPDEST"}),
657 context: Some(json!({
658 "code": {
659 "source": {"id": 0},
660 "range": {"offset": offset, "length": 1}
661 }
662 })),
663 })
664 .collect();
665 let info = EthdebugInfo {
666 compilation: json!({}),
667 contract_name: "C".to_owned(),
668 environment: "call".to_owned(),
669 instructions,
670 sources: BTreeMap::from([(0, path.to_owned())]),
671 variable_locations: BTreeMap::new(),
672 };
673 ContractDebugInfo::new(None, "C", info, BTreeMap::from([(0, SOURCE.to_owned())]))
674 }
675
676 #[test]
677 fn different_bytecode_with_the_same_source_steps_matches() {
678 let first = SOURCE.find("uint256").expect("statement");
679 let second = SOURCE.find("x++").expect("statement");
680 let reference = capture_debug_trace(
681 &trace(&[0, 1], "0x01"),
682 vec![contract(&[0, 1], &[first, second], "src/C.sol")],
683 );
684 let candidate = capture_debug_trace(
685 &trace(&[20, 24], "0x01"),
686 vec![contract(&[20, 24], &[first, second], "/checkout/src/C.sol")],
687 );
688
689 let report = compare_debug_traces(&reference, &candidate, DebugDiffMode::Steps, 8);
690
691 assert!(report.equivalent);
692 assert_eq!(report.difference_count, 0);
693 }
694
695 #[test]
696 fn changed_steps_and_execution_are_reported_separately() {
697 let first = SOURCE.find("uint256").expect("statement");
698 let second = SOURCE.find("x++").expect("statement");
699 let reference = capture_debug_trace(
700 &trace(&[0, 1], "0x01"),
701 vec![contract(&[0, 1], &[first, second], "C.sol")],
702 );
703 let candidate = capture_debug_trace(
704 &trace(&[20, 24], "0x02"),
705 vec![contract(&[20, 24], &[second, first], "C.sol")],
706 );
707
708 let report = compare_debug_traces(&reference, &candidate, DebugDiffMode::Steps, 1);
709
710 assert!(!report.equivalent);
711 assert!(!report.execution_equivalent);
712 assert_eq!(report.execution_differences, ["return data differs"]);
713 assert_eq!(report.difference_count, 2);
714 assert_eq!(report.differences.len(), 1);
715 assert!(report.differences_truncated);
716 assert_eq!(report.differences[0].kind, DebugDifferenceKind::Changed);
717 }
718
719 #[test]
720 fn coverage_ignores_order_and_repetition() {
721 let first = SOURCE.find("uint256").expect("statement");
722 let second = SOURCE.find("x++").expect("statement");
723 let reference = capture_debug_trace(
724 &trace(&[0, 0, 1, 1], "0x"),
725 vec![contract(&[0, 1], &[first, second], "C.sol")],
726 );
727 let candidate = capture_debug_trace(
728 &trace(&[20, 20, 24, 24], "0x"),
729 vec![contract(&[20, 24], &[second, first], "C.sol")],
730 );
731
732 let report = compare_debug_traces(&reference, &candidate, DebugDiffMode::Coverage, 8);
733
734 assert!(report.equivalent);
735 }
736
737 #[test]
738 fn empty_source_traces_are_not_comparable() {
739 let reference = capture_debug_trace(&trace(&[99], "0x"), Vec::new());
740 let candidate = capture_debug_trace(&trace(&[100], "0x"), Vec::new());
741
742 let report = compare_debug_traces(&reference, &candidate, DebugDiffMode::Steps, 8);
743
744 assert!(!report.equivalent);
745 assert!(!report.comparable);
746 assert!(report
747 .diagnostics
748 .iter()
749 .any(|message| message == "reference trace has no source steps"));
750 }
751
752 #[test]
753 fn span_mode_checks_exact_ranges_and_modifier_depth() {
754 let offset = SOURCE.find("uint256").expect("statement");
755 let reference =
756 capture_debug_trace(&trace(&[0], "0x"), vec![contract(&[0], &[offset], "C.sol")]);
757 let mut candidate = reference.clone();
758 candidate.events[0].length += 1;
759 candidate.events[0].modifier_depth = Some(1);
760
761 assert!(compare_debug_traces(&reference, &candidate, DebugDiffMode::Steps, 8).equivalent);
762 assert!(!compare_debug_traces(&reference, &candidate, DebugDiffMode::Spans, 8).equivalent);
763 }
764
765 #[test]
766 fn execution_comparison_normalizes_hex_spelling() {
767 let offset = SOURCE.find("uint256").expect("statement");
768 let reference = capture_debug_trace(
769 &trace(&[0], "0xAB"),
770 vec![contract(&[0], &[offset], "C.sol")],
771 );
772 let mut candidate_trace = trace(&[20], "0Xab");
773 candidate_trace.value = "0x00".to_owned();
774 let candidate =
775 capture_debug_trace(&candidate_trace, vec![contract(&[20], &[offset], "C.sol")]);
776
777 assert!(compare_debug_traces(&reference, &candidate, DebugDiffMode::Steps, 8).equivalent);
778 }
779
780 #[test]
781 fn mismatched_opcodes_and_missing_programs_cannot_pass() {
782 let offset = SOURCE.find("uint256").expect("statement");
783 let reference =
784 capture_debug_trace(&trace(&[0], "0x"), vec![contract(&[0], &[offset], "C.sol")]);
785 let mut wrong = trace(&[0], "0x");
786 wrong.steps[0].op = "INVALID".into();
787 let candidate = capture_debug_trace(&wrong, vec![contract(&[0], &[offset], "C.sol")]);
788 let report = compare_debug_traces(&reference, &candidate, DebugDiffMode::Steps, 8);
789 assert!(!report.equivalent);
790 assert!(!report.comparable);
791 assert_eq!(report.difference_count, 0);
792 assert!(report.diagnostics[0].contains("trace opcode `INVALID`"));
793 }
794
795 #[test]
796 fn source_paths_and_contents_both_participate_in_identity() {
797 let offset = SOURCE.find("uint256").expect("statement");
798 let reference = capture_debug_trace(
799 &trace(&[0], "0x"),
800 vec![contract(&[0], &[offset], "a/C.sol")],
801 );
802 let candidate = capture_debug_trace(
803 &trace(&[0], "0x"),
804 vec![contract(&[0], &[offset], "b/C.sol")],
805 );
806 for mode in [DebugDiffMode::Steps, DebugDiffMode::Coverage] {
807 assert!(!compare_debug_traces(&reference, &candidate, mode, 8).equivalent);
808 }
809 let mut changed = contract(&[0], &[offset], "a/C.sol");
810 changed
811 .source_contents
812 .insert(0, SOURCE.replace("x = 1", "x = 2"));
813 let candidate = capture_debug_trace(&trace(&[0], "0x"), vec![changed]);
814 assert!(!compare_debug_traces(&reference, &candidate, DebugDiffMode::Steps, 8).equivalent);
815 }
816
817 #[test]
818 fn constructors_compare_arguments_and_revert_data_not_initcode() {
819 let offset = SOURCE.find("uint256").expect("statement");
820 let mut left = trace(&[0], "0x6000");
821 left.to_addr = None;
822 left.input_data = "0x60000042".to_owned();
823 let mut right = left.clone();
824 right.input_data = "0x60010042".to_owned();
825 right.output = "0x6001".to_owned();
826 let mut reference = capture_debug_trace(&left, vec![contract(&[0], &[offset], "C.sol")]);
827 let mut candidate = capture_debug_trace(&right, vec![contract(&[0], &[offset], "C.sol")]);
828 assert!(!compare_debug_traces(&reference, &candidate, DebugDiffMode::Steps, 8).equivalent);
829 assert!(candidate.set_constructor_args("0x99").is_err());
830 reference.set_constructor_args("0x0042").expect("arguments");
831 candidate.set_constructor_args("0x0042").expect("arguments");
832 assert!(compare_debug_traces(&reference, &candidate, DebugDiffMode::Steps, 8).equivalent);
833 reference.execution.success = false;
834 candidate.execution.success = false;
835 assert!(
836 !compare_debug_traces(&reference, &candidate, DebugDiffMode::Steps, 8)
837 .execution_equivalent
838 );
839 }
840
841 #[test]
842 fn checkpoints_cannot_hide_an_unreached_statement() {
843 let first = SOURCE.find("uint256").expect("statement");
844 let second = SOURCE.find("x++").expect("statement");
845 let mut reference = capture_debug_trace(
846 &trace(&[0, 1], "0x"),
847 vec![contract(&[0, 1], &[first, second], "C.sol")],
848 );
849 let mut candidate =
850 capture_debug_trace(&trace(&[0], "0x"), vec![contract(&[0], &[first], "C.sol")]);
851 let checkpoints = [DebugCheckpoint {
852 source: "C.sol".to_owned(),
853 line: 4,
854 }];
855 reference.retain_checkpoints(&checkpoints);
856 candidate.retain_checkpoints(&checkpoints);
857 let report = compare_debug_traces(&reference, &candidate, DebugDiffMode::Coverage, 8);
858 assert!(!report.equivalent);
859 assert!(!report.comparable);
860 assert_eq!(reference.events.len(), 1);
861 assert_eq!(candidate.events.len(), 0);
862 assert!(report
863 .diagnostics
864 .iter()
865 .any(|message| message == "candidate: checkpoint C.sol:4 was not reached"));
866 }
867
868 #[test]
869 fn partial_artifacts_and_invalid_ranges_cannot_pass() {
870 let offset = SOURCE.find("uint256").expect("statement");
871 let reference =
872 capture_debug_trace(&trace(&[0], "0x"), vec![contract(&[0], &[offset], "C.sol")]);
873 let partial = capture_debug_trace(
874 &trace(&[0, 99], "0x"),
875 vec![contract(&[0], &[offset], "C.sol")],
876 );
877 assert!(!compare_debug_traces(&reference, &partial, DebugDiffMode::Steps, 8).comparable);
878 let invalid = capture_debug_trace(
879 &trace(&[0], "0x"),
880 vec![contract(&[0], &[SOURCE.len() + 1], "C.sol")],
881 );
882 assert!(!compare_debug_traces(&reference, &invalid, DebugDiffMode::Steps, 8).comparable);
883 }
884
885 #[test]
886 fn a_single_instruction_body_is_not_smoothed_out() {
887 let declaration = SOURCE.find("function").expect("declaration");
888 let statement = SOURCE.find("uint256").expect("statement");
889 let mut captured = capture_debug_trace(
890 &trace(&[0, 1, 2], "0x"),
891 vec![contract(
892 &[0, 1, 2],
893 &[declaration, statement, declaration],
894 "C.sol",
895 )],
896 );
897 captured.retain_checkpoints(&[DebugCheckpoint {
898 source: "C.sol".to_owned(),
899 line: 3,
900 }]);
901 assert!(captured.diagnostics.is_empty());
902 assert_eq!(captured.events.len(), 1);
903 assert_eq!(captured.events[0].pc, 1);
904 }
905}