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.step(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.mnemonic())) {
252 diagnostics.insert(format!(
253 "trace opcode `{}` at PC {} disagrees with artifact `{}` in `{}`",
254 step.mnemonic(),
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::{source_range, 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)| {
655 let code = source_range(0, *offset as u64, 1);
656 Instruction::new(*pc, "JUMPDEST", Some(json!({"code": code})))
657 })
658 .collect();
659 let info = EthdebugInfo {
660 compilation: json!({}),
661 contract_name: "C".to_owned(),
662 environment: "call".to_owned(),
663 instructions,
664 sources: BTreeMap::from([(0, path.to_owned())]),
665 variable_locations: BTreeMap::new(),
666 };
667 ContractDebugInfo::new(None, "C", info, BTreeMap::from([(0, SOURCE.to_owned())]))
668 }
669
670 #[test]
671 fn different_bytecode_with_the_same_source_steps_matches() {
672 let first = SOURCE.find("uint256").expect("statement");
673 let second = SOURCE.find("x++").expect("statement");
674 let reference = capture_debug_trace(
675 &trace(&[0, 1], "0x01"),
676 vec![contract(&[0, 1], &[first, second], "src/C.sol")],
677 );
678 let candidate = capture_debug_trace(
679 &trace(&[20, 24], "0x01"),
680 vec![contract(&[20, 24], &[first, second], "/checkout/src/C.sol")],
681 );
682
683 let report = compare_debug_traces(&reference, &candidate, DebugDiffMode::Steps, 8);
684
685 assert!(report.equivalent);
686 assert_eq!(report.difference_count, 0);
687 }
688
689 #[test]
690 fn changed_steps_and_execution_are_reported_separately() {
691 let first = SOURCE.find("uint256").expect("statement");
692 let second = SOURCE.find("x++").expect("statement");
693 let reference = capture_debug_trace(
694 &trace(&[0, 1], "0x01"),
695 vec![contract(&[0, 1], &[first, second], "C.sol")],
696 );
697 let candidate = capture_debug_trace(
698 &trace(&[20, 24], "0x02"),
699 vec![contract(&[20, 24], &[second, first], "C.sol")],
700 );
701
702 let report = compare_debug_traces(&reference, &candidate, DebugDiffMode::Steps, 1);
703
704 assert!(!report.equivalent);
705 assert!(!report.execution_equivalent);
706 assert_eq!(report.execution_differences, ["return data differs"]);
707 assert_eq!(report.difference_count, 2);
708 assert_eq!(report.differences.len(), 1);
709 assert!(report.differences_truncated);
710 assert_eq!(report.differences[0].kind, DebugDifferenceKind::Changed);
711 }
712
713 #[test]
714 fn coverage_ignores_order_and_repetition() {
715 let first = SOURCE.find("uint256").expect("statement");
716 let second = SOURCE.find("x++").expect("statement");
717 let reference = capture_debug_trace(
718 &trace(&[0, 0, 1, 1], "0x"),
719 vec![contract(&[0, 1], &[first, second], "C.sol")],
720 );
721 let candidate = capture_debug_trace(
722 &trace(&[20, 20, 24, 24], "0x"),
723 vec![contract(&[20, 24], &[second, first], "C.sol")],
724 );
725
726 let report = compare_debug_traces(&reference, &candidate, DebugDiffMode::Coverage, 8);
727
728 assert!(report.equivalent);
729 }
730
731 #[test]
732 fn empty_source_traces_are_not_comparable() {
733 let reference = capture_debug_trace(&trace(&[99], "0x"), Vec::new());
734 let candidate = capture_debug_trace(&trace(&[100], "0x"), Vec::new());
735
736 let report = compare_debug_traces(&reference, &candidate, DebugDiffMode::Steps, 8);
737
738 assert!(!report.equivalent);
739 assert!(!report.comparable);
740 assert!(report
741 .diagnostics
742 .iter()
743 .any(|message| message == "reference trace has no source steps"));
744 }
745
746 #[test]
747 fn span_mode_checks_exact_ranges_and_modifier_depth() {
748 let offset = SOURCE.find("uint256").expect("statement");
749 let reference =
750 capture_debug_trace(&trace(&[0], "0x"), vec![contract(&[0], &[offset], "C.sol")]);
751 let mut candidate = reference.clone();
752 candidate.events[0].length += 1;
753 candidate.events[0].modifier_depth = Some(1);
754
755 assert!(compare_debug_traces(&reference, &candidate, DebugDiffMode::Steps, 8).equivalent);
756 assert!(!compare_debug_traces(&reference, &candidate, DebugDiffMode::Spans, 8).equivalent);
757 }
758
759 #[test]
760 fn execution_comparison_normalizes_hex_spelling() {
761 let offset = SOURCE.find("uint256").expect("statement");
762 let reference = capture_debug_trace(
763 &trace(&[0], "0xAB"),
764 vec![contract(&[0], &[offset], "C.sol")],
765 );
766 let mut candidate_trace = trace(&[20], "0Xab");
767 candidate_trace.value = "0x00".to_owned();
768 let candidate =
769 capture_debug_trace(&candidate_trace, vec![contract(&[20], &[offset], "C.sol")]);
770
771 assert!(compare_debug_traces(&reference, &candidate, DebugDiffMode::Steps, 8).equivalent);
772 }
773
774 #[test]
775 fn mismatched_opcodes_and_missing_programs_cannot_pass() {
776 let offset = SOURCE.find("uint256").expect("statement");
777 let reference =
778 capture_debug_trace(&trace(&[0], "0x"), vec![contract(&[0], &[offset], "C.sol")]);
779 let mut wrong = trace(&[0], "0x");
780 let mut steps = wrong.steps.to_steps();
781 steps[0].op = "INVALID".into();
782 wrong.steps = steps.into();
783 let candidate = capture_debug_trace(&wrong, vec![contract(&[0], &[offset], "C.sol")]);
784 let report = compare_debug_traces(&reference, &candidate, DebugDiffMode::Steps, 8);
785 assert!(!report.equivalent);
786 assert!(!report.comparable);
787 assert_eq!(report.difference_count, 0);
788 assert!(report.diagnostics[0].contains("trace opcode `INVALID`"));
789 }
790
791 #[test]
792 fn source_paths_and_contents_both_participate_in_identity() {
793 let offset = SOURCE.find("uint256").expect("statement");
794 let reference = capture_debug_trace(
795 &trace(&[0], "0x"),
796 vec![contract(&[0], &[offset], "a/C.sol")],
797 );
798 let candidate = capture_debug_trace(
799 &trace(&[0], "0x"),
800 vec![contract(&[0], &[offset], "b/C.sol")],
801 );
802 for mode in [DebugDiffMode::Steps, DebugDiffMode::Coverage] {
803 assert!(!compare_debug_traces(&reference, &candidate, mode, 8).equivalent);
804 }
805 let mut changed = contract(&[0], &[offset], "a/C.sol");
806 changed
807 .source_contents
808 .insert(0, SOURCE.replace("x = 1", "x = 2"));
809 let candidate = capture_debug_trace(&trace(&[0], "0x"), vec![changed]);
810 assert!(!compare_debug_traces(&reference, &candidate, DebugDiffMode::Steps, 8).equivalent);
811 }
812
813 #[test]
814 fn constructors_compare_arguments_and_revert_data_not_initcode() {
815 let offset = SOURCE.find("uint256").expect("statement");
816 let mut left = trace(&[0], "0x6000");
817 left.to_addr = None;
818 left.input_data = "0x60000042".to_owned();
819 let mut right = left.clone();
820 right.input_data = "0x60010042".to_owned();
821 right.output = "0x6001".to_owned();
822 let mut reference = capture_debug_trace(&left, vec![contract(&[0], &[offset], "C.sol")]);
823 let mut candidate = capture_debug_trace(&right, vec![contract(&[0], &[offset], "C.sol")]);
824 assert!(!compare_debug_traces(&reference, &candidate, DebugDiffMode::Steps, 8).equivalent);
825 assert!(candidate.set_constructor_args("0x99").is_err());
826 reference.set_constructor_args("0x0042").expect("arguments");
827 candidate.set_constructor_args("0x0042").expect("arguments");
828 assert!(compare_debug_traces(&reference, &candidate, DebugDiffMode::Steps, 8).equivalent);
829 reference.execution.success = false;
830 candidate.execution.success = false;
831 assert!(
832 !compare_debug_traces(&reference, &candidate, DebugDiffMode::Steps, 8)
833 .execution_equivalent
834 );
835 }
836
837 #[test]
838 fn checkpoints_cannot_hide_an_unreached_statement() {
839 let first = SOURCE.find("uint256").expect("statement");
840 let second = SOURCE.find("x++").expect("statement");
841 let mut reference = capture_debug_trace(
842 &trace(&[0, 1], "0x"),
843 vec![contract(&[0, 1], &[first, second], "C.sol")],
844 );
845 let mut candidate =
846 capture_debug_trace(&trace(&[0], "0x"), vec![contract(&[0], &[first], "C.sol")]);
847 let checkpoints = [DebugCheckpoint {
848 source: "C.sol".to_owned(),
849 line: 4,
850 }];
851 reference.retain_checkpoints(&checkpoints);
852 candidate.retain_checkpoints(&checkpoints);
853 let report = compare_debug_traces(&reference, &candidate, DebugDiffMode::Coverage, 8);
854 assert!(!report.equivalent);
855 assert!(!report.comparable);
856 assert_eq!(reference.events.len(), 1);
857 assert_eq!(candidate.events.len(), 0);
858 assert!(report
859 .diagnostics
860 .iter()
861 .any(|message| message == "candidate: checkpoint C.sol:4 was not reached"));
862 }
863
864 #[test]
865 fn partial_artifacts_and_invalid_ranges_cannot_pass() {
866 let offset = SOURCE.find("uint256").expect("statement");
867 let reference =
868 capture_debug_trace(&trace(&[0], "0x"), vec![contract(&[0], &[offset], "C.sol")]);
869 let partial = capture_debug_trace(
870 &trace(&[0, 99], "0x"),
871 vec![contract(&[0], &[offset], "C.sol")],
872 );
873 assert!(!compare_debug_traces(&reference, &partial, DebugDiffMode::Steps, 8).comparable);
874 let invalid = capture_debug_trace(
875 &trace(&[0], "0x"),
876 vec![contract(&[0], &[SOURCE.len() + 1], "C.sol")],
877 );
878 assert!(!compare_debug_traces(&reference, &invalid, DebugDiffMode::Steps, 8).comparable);
879 }
880
881 #[test]
882 fn a_single_instruction_body_is_not_smoothed_out() {
883 let declaration = SOURCE.find("function").expect("declaration");
884 let statement = SOURCE.find("uint256").expect("statement");
885 let mut captured = capture_debug_trace(
886 &trace(&[0, 1, 2], "0x"),
887 vec![contract(
888 &[0, 1, 2],
889 &[declaration, statement, declaration],
890 "C.sol",
891 )],
892 );
893 captured.retain_checkpoints(&[DebugCheckpoint {
894 source: "C.sol".to_owned(),
895 line: 3,
896 }]);
897 assert!(captured.diagnostics.is_empty());
898 assert_eq!(captured.events.len(), 1);
899 assert_eq!(captured.events[0].pc, 1);
900 }
901}