1use super::test;
2use crate::result::{SuiteTestResult, TestKindReport, TestOutcome};
3use alloy_primitives::{U256, map::HashMap};
4use clap::{Parser, ValueHint, builder::RangedU64ValueParser};
5use comfy_table::{
6 Cell, Color, Row, Table,
7 presets::{ASCII_FULL, ASCII_MARKDOWN},
8};
9use eyre::{Context, Result};
10use foundry_cli::utils::STATIC_FUZZ_SEED;
11use foundry_common::shell;
12use regex::Regex;
13use std::{
14 cmp::Ordering,
15 fs,
16 io::{self, BufRead},
17 path::{Path, PathBuf},
18 str::FromStr,
19 sync::LazyLock,
20};
21use yansi::Paint;
22
23pub static RE_BASIC_SNAPSHOT_ENTRY: LazyLock<Regex> = LazyLock::new(|| {
26 Regex::new(r"(?P<file>(.*?)):(?P<sig>(\w+)\s*\((.*?)\))\s*\(((gas:)?\s*(?P<gas>\d+)|(runs:\s*(?P<runs>\d+),\s*μ:\s*(?P<avg>\d+),\s*~:\s*(?P<med>\d+))|(runs:\s*(?P<invruns>\d+),\s*calls:\s*(?P<calls>\d+),\s*reverts:\s*(?P<reverts>\d+)))\)").unwrap()
27});
28
29#[derive(Clone, Debug, Parser)]
31pub struct GasSnapshotArgs {
32 #[arg(
36 conflicts_with = "snap",
37 long,
38 value_hint = ValueHint::FilePath,
39 value_name = "SNAPSHOT_FILE",
40 )]
41 diff: Option<Option<PathBuf>>,
42
43 #[arg(
49 conflicts_with = "diff",
50 long,
51 value_hint = ValueHint::FilePath,
52 value_name = "SNAPSHOT_FILE",
53 )]
54 check: Option<Option<PathBuf>>,
55
56 #[arg(long, hide(true))]
59 format: Option<Format>,
60
61 #[arg(
63 long,
64 default_value = ".gas-snapshot",
65 value_hint = ValueHint::FilePath,
66 value_name = "FILE",
67 )]
68 snap: PathBuf,
69
70 #[arg(
72 long,
73 value_parser = RangedU64ValueParser::<u32>::new().range(0..100),
74 value_name = "SNAPSHOT_THRESHOLD"
75 )]
76 tolerance: Option<u32>,
77
78 #[arg(long, value_name = "ORDER")]
80 diff_sort: Option<DiffSortOrder>,
81
82 #[command(flatten)]
84 pub(crate) test: test::TestArgs,
85
86 #[command(flatten)]
88 config: GasSnapshotConfig,
89}
90
91impl GasSnapshotArgs {
92 pub const fn is_watch(&self) -> bool {
94 self.test.is_watch()
95 }
96
97 pub(crate) fn watchexec_config(&self) -> Result<watchexec::Config> {
99 self.test.watchexec_config()
100 }
101
102 pub async fn run(mut self) -> Result<()> {
103 if self.test.fuzz_seed.is_none() {
106 self.test.fuzz_seed = Some(U256::from_be_bytes(STATIC_FUZZ_SEED));
107 }
108
109 let outcome = self.test.compile_and_run().await?;
110 if !shell::is_quiet()
111 && !outcome.allow_failure
112 && self.diff.is_none()
113 && self.check.is_none()
114 && outcome.failed() > 0
115 {
116 sh_eprintln!(
117 "Error: gas snapshot file \"{}\" was not written because the test run failed",
118 self.snap.display()
119 )?;
120 }
121 outcome.ensure_ok(false)?;
122 let tests = self.config.apply(outcome);
123
124 if let Some(path) = self.diff {
125 let snap = path.as_ref().unwrap_or(&self.snap);
126 let snaps = read_gas_snapshot(snap)?;
127 diff(tests, snaps, self.diff_sort.unwrap_or_default())?;
128 } else if let Some(path) = self.check {
129 let snap = path.as_ref().unwrap_or(&self.snap);
130 let snaps = read_gas_snapshot(snap)?;
131 let code = if check(tests, snaps, self.tolerance) { 0 } else { 1 };
132 std::process::exit(code)
133 } else {
134 if matches!(self.format, Some(Format::Table)) {
135 let table = build_gas_snapshot_table(&tests);
136 sh_println!("\n{}", table)?;
137 }
138 write_to_gas_snapshot_file(&tests, self.snap, self.format)?;
139 }
140 Ok(())
141 }
142}
143
144#[derive(Clone, Debug)]
146pub enum Format {
147 Table,
148}
149
150impl FromStr for Format {
151 type Err = String;
152
153 fn from_str(s: &str) -> Result<Self, Self::Err> {
154 match s {
155 "t" | "table" => Ok(Self::Table),
156 _ => Err(format!("Unrecognized format `{s}`")),
157 }
158 }
159}
160
161#[derive(Clone, Debug, Default, Parser)]
163struct GasSnapshotConfig {
164 #[arg(long)]
166 asc: bool,
167
168 #[arg(conflicts_with = "asc", long)]
170 desc: bool,
171
172 #[arg(long, value_name = "MIN_GAS")]
174 min: Option<u64>,
175
176 #[arg(long, value_name = "MAX_GAS")]
178 max: Option<u64>,
179}
180
181#[derive(Clone, Debug, Default, clap::ValueEnum)]
183enum DiffSortOrder {
184 #[default]
186 Percentage,
187 PercentageDesc,
189 Absolute,
191 AbsoluteDesc,
193}
194
195impl GasSnapshotConfig {
196 const fn is_in_gas_range(&self, gas_used: u64) -> bool {
197 if let Some(min) = self.min
198 && gas_used < min
199 {
200 return false;
201 }
202 if let Some(max) = self.max
203 && gas_used > max
204 {
205 return false;
206 }
207 true
208 }
209
210 fn apply(&self, outcome: TestOutcome) -> Vec<SuiteTestResult> {
211 let mut tests = outcome
212 .into_tests()
213 .filter(|test| self.is_in_gas_range(test.gas_used()))
214 .flat_map(expand_invariant_snapshot_entries)
215 .collect::<Vec<_>>();
216
217 if self.asc {
218 tests.sort_by_key(|a| a.gas_used());
219 } else if self.desc {
220 tests.sort_by_key(|b| std::cmp::Reverse(b.gas_used()))
221 }
222
223 tests
224 }
225}
226
227fn expand_invariant_snapshot_entries(test: SuiteTestResult) -> Vec<SuiteTestResult> {
229 if !test.result.kind.is_invariant() || test.result.invariant_predicate_results.len() <= 1 {
230 return vec![test];
231 }
232
233 test.result
234 .invariant_predicate_results
235 .iter()
236 .map(|predicate| {
237 let mut expanded = test.clone();
238 expanded.signature = format!("{}()", predicate.name);
239 expanded
240 })
241 .collect()
242}
243
244#[derive(Clone, Debug, PartialEq, Eq)]
251pub struct GasSnapshotEntry {
252 pub contract_name: String,
253 pub signature: String,
254 pub gas_used: TestKindReport,
255}
256
257impl FromStr for GasSnapshotEntry {
258 type Err = String;
259
260 fn from_str(s: &str) -> Result<Self, Self::Err> {
261 let cap = RE_BASIC_SNAPSHOT_ENTRY
262 .captures(s)
263 .ok_or_else(|| format!("Could not extract Snapshot Entry for {s}"))?;
264 let contract_name = cap
265 .name("file")
266 .ok_or_else(|| format!("Could not extract Snapshot Entry for {s}"))?
267 .as_str()
268 .to_string();
269 let signature = cap
270 .name("sig")
271 .ok_or_else(|| format!("Could not extract Snapshot Entry for {s}"))?
272 .as_str()
273 .to_string();
274
275 let gas_used = if let Some(gas) = cap.name("gas") {
276 TestKindReport::Unit { gas: parse_snapshot_value(gas.as_str(), "gas")? }
277 } else if let Some(runs) = cap.name("runs") {
278 let avg = cap
279 .name("avg")
280 .ok_or_else(|| format!("Could not extract Snapshot Entry for {s}"))?;
281 let med = cap
282 .name("med")
283 .ok_or_else(|| format!("Could not extract Snapshot Entry for {s}"))?;
284 TestKindReport::Fuzz {
285 runs: parse_snapshot_value(runs.as_str(), "runs")?,
286 median_gas: parse_snapshot_value(med.as_str(), "median gas")?,
287 mean_gas: parse_snapshot_value(avg.as_str(), "mean gas")?,
288 failed_corpus_replays: 0,
289 }
290 } else {
291 let runs = cap
292 .name("invruns")
293 .ok_or_else(|| format!("Could not extract Snapshot Entry for {s}"))?;
294 let calls = cap
295 .name("calls")
296 .ok_or_else(|| format!("Could not extract Snapshot Entry for {s}"))?;
297 let reverts = cap
298 .name("reverts")
299 .ok_or_else(|| format!("Could not extract Snapshot Entry for {s}"))?;
300 TestKindReport::Invariant {
301 runs: parse_snapshot_value(runs.as_str(), "runs")?,
302 calls: parse_snapshot_value(calls.as_str(), "calls")?,
303 reverts: parse_snapshot_value(reverts.as_str(), "reverts")?,
304 failed_corpus_replays: 0,
305 optimization_best_value: None,
306 }
307 };
308
309 Ok(Self { contract_name, signature, gas_used })
310 }
311}
312
313fn parse_snapshot_value<T: FromStr>(value: &str, field: &str) -> Result<T, String> {
314 value.parse().map_err(|_| format!("invalid {field} value `{value}` in gas snapshot"))
315}
316
317fn read_gas_snapshot(path: impl AsRef<Path>) -> Result<Vec<GasSnapshotEntry>> {
319 let path = path.as_ref();
320 let mut entries = Vec::new();
321 for (line_number, line) in io::BufReader::new(
322 fs::File::open(path)
323 .wrap_err(format!("failed to read snapshot file \"{}\"", path.display()))?,
324 )
325 .lines()
326 .enumerate()
327 {
328 let line_number = line_number + 1;
329 let line = line.wrap_err_with(|| {
330 format!("failed to read snapshot file \"{}\" at line {line_number}", path.display())
331 })?;
332 let entry = GasSnapshotEntry::from_str(&line)
333 .map_err(|err| eyre::eyre!(err))
334 .wrap_err_with(|| {
335 format!(
336 "failed to parse snapshot file \"{}\" at line {line_number}",
337 path.display()
338 )
339 })?;
340 entries.push(entry);
341 }
342 Ok(entries)
343}
344
345fn write_to_gas_snapshot_file(
347 tests: &[SuiteTestResult],
348 path: impl AsRef<Path>,
349 _format: Option<Format>,
350) -> Result<()> {
351 let mut reports = tests
352 .iter()
353 .map(|test| {
354 format!("{}:{} {}", test.contract_name(), test.signature, test.result.kind.report())
355 })
356 .collect::<Vec<_>>();
357
358 reports.sort();
360
361 let content = reports.join("\n");
362 Ok(fs::write(path, content)?)
363}
364
365fn build_gas_snapshot_table(tests: &[SuiteTestResult]) -> Table {
366 let mut table = Table::new();
367 if shell::is_markdown() {
368 table.load_style(ASCII_MARKDOWN);
369 } else {
370 table.load_style(ASCII_FULL.with_rounded_corners());
371 }
372
373 table.set_header(vec![
374 Cell::new("Contract").fg(Color::Cyan),
375 Cell::new("Signature").fg(Color::Cyan),
376 Cell::new("Report").fg(Color::Cyan),
377 ]);
378
379 for test in tests {
380 let mut row = Row::new();
381 row.add_cell(Cell::new(test.contract_name()));
382 row.add_cell(Cell::new(&test.signature));
383 row.add_cell(Cell::new(test.result.kind.report()));
384 table.add_row(row);
385 }
386
387 table
388}
389
390#[derive(Clone, Debug, PartialEq, Eq)]
392pub struct GasSnapshotDiff {
393 pub signature: String,
394 pub source_gas_used: TestKindReport,
395 pub target_gas_used: TestKindReport,
396}
397
398impl GasSnapshotDiff {
399 const fn gas_change(&self) -> i128 {
404 self.source_gas_used.gas() as i128 - self.target_gas_used.gas() as i128
405 }
406
407 fn gas_diff(&self) -> f64 {
409 let target_gas = self.target_gas_used.gas();
410 if target_gas > 0 {
411 self.gas_change() as f64 / target_gas as f64
412 } else if self.source_gas_used.gas() == 0 {
413 0.0
415 } else {
416 f64::INFINITY
418 }
419 }
420}
421
422fn check(
426 tests: Vec<SuiteTestResult>,
427 snaps: Vec<GasSnapshotEntry>,
428 tolerance: Option<u32>,
429) -> bool {
430 let snaps = snaps
431 .into_iter()
432 .map(|s| ((s.contract_name, s.signature), s.gas_used))
433 .collect::<HashMap<_, _>>();
434 let mut has_diff = false;
435 for test in tests {
436 if let Some(target_gas) =
437 snaps.get(&(test.contract_name().to_string(), test.signature.clone())).cloned()
438 {
439 let source_gas = test.result.kind.report();
440 if !within_tolerance(source_gas.gas(), target_gas.gas(), tolerance) {
441 let _ = sh_eprintln!(
442 "Diff in \"{}::{}\": consumed \"{}\" gas, expected \"{}\" gas ",
443 test.contract_name(),
444 test.signature,
445 source_gas,
446 target_gas
447 );
448 has_diff = true;
449 }
450 } else {
451 let _ = sh_eprintln!(
452 "No matching snapshot entry found for \"{}::{}\" in snapshot file",
453 test.contract_name(),
454 test.signature
455 );
456 has_diff = true;
457 }
458 }
459 !has_diff
460}
461
462fn diff(
464 tests: Vec<SuiteTestResult>,
465 snaps: Vec<GasSnapshotEntry>,
466 sort_order: DiffSortOrder,
467) -> Result<()> {
468 let snaps = snaps
469 .into_iter()
470 .map(|s| ((s.contract_name, s.signature), s.gas_used))
471 .collect::<HashMap<_, _>>();
472 let mut diffs = Vec::with_capacity(tests.len());
473 let mut new_tests = Vec::new();
474
475 for test in tests {
476 if let Some(target_gas_used) =
477 snaps.get(&(test.contract_name().to_string(), test.signature.clone())).cloned()
478 {
479 diffs.push(GasSnapshotDiff {
480 source_gas_used: test.result.kind.report(),
481 signature: format!("{}::{}", test.contract_name(), test.signature),
482 target_gas_used,
483 });
484 } else {
485 new_tests.push(format!("{}::{}", test.contract_name(), test.signature));
487 }
488 }
489
490 let mut increased = 0;
491 let mut decreased = 0;
492 let mut unchanged = 0;
493 let mut overall_gas_change = 0i128;
494 let mut overall_gas_used = 0i128;
495
496 match sort_order {
498 DiffSortOrder::Percentage => {
499 diffs.sort_by(|a, b| a.gas_diff().abs().total_cmp(&b.gas_diff().abs()));
501 }
502 DiffSortOrder::PercentageDesc => {
503 diffs.sort_by(|a, b| b.gas_diff().abs().total_cmp(&a.gas_diff().abs()));
505 }
506 DiffSortOrder::Absolute => {
507 diffs.sort_by_key(|d| d.gas_change().abs());
509 }
510 DiffSortOrder::AbsoluteDesc => {
511 diffs.sort_by_key(|d| std::cmp::Reverse(d.gas_change().abs()));
513 }
514 }
515
516 for diff in &diffs {
517 let gas_change = diff.gas_change();
518 overall_gas_change += gas_change;
519 overall_gas_used += diff.target_gas_used.gas() as i128;
520 let gas_diff = diff.gas_diff();
521
522 if gas_change > 0 {
524 increased += 1;
525 } else if gas_change < 0 {
526 decreased += 1;
527 } else {
528 unchanged += 1;
529 }
530
531 let icon = if gas_change > 0 {
533 "↑".red().to_string()
534 } else if gas_change < 0 {
535 "↓".green().to_string()
536 } else {
537 "━".to_string()
538 };
539
540 sh_println!(
541 "{} {} (gas: {} → {} | {} {})",
542 icon,
543 diff.signature,
544 diff.target_gas_used.gas(),
545 diff.source_gas_used.gas(),
546 fmt_change(gas_change),
547 fmt_pct_change(gas_diff)
548 )?;
549 }
550
551 if !new_tests.is_empty() {
553 sh_eprintln!("\n{}", "New tests:".yellow())?;
554 for test in new_tests {
555 sh_eprintln!(" {} {}", "+".green(), test)?;
556 }
557 }
558
559 sh_eprintln!("\n{}", "-".repeat(80))?;
561
562 let overall_gas_diff = if overall_gas_used > 0 {
563 overall_gas_change as f64 / overall_gas_used as f64
564 } else {
565 0.0
566 };
567
568 sh_eprintln!(
569 "Total tests: {}, {} {}, {} {}, {} {}",
570 diffs.len(),
571 "↑".red().to_string(),
572 increased,
573 "↓".green().to_string(),
574 decreased,
575 "━",
576 unchanged
577 )?;
578 sh_eprintln!(
579 "Overall gas change: {} ({})",
580 fmt_change(overall_gas_change),
581 fmt_pct_change(overall_gas_diff)
582 )?;
583 Ok(())
584}
585
586fn fmt_pct_change(change: f64) -> String {
587 let change_pct = change * 100.0;
588 match change.total_cmp(&0.0) {
589 Ordering::Less => format!("{change_pct:.3}%").green().to_string(),
590 Ordering::Equal => {
591 format!("{change_pct:.3}%")
592 }
593 Ordering::Greater => format!("{change_pct:.3}%").red().to_string(),
594 }
595}
596
597fn fmt_change(change: i128) -> String {
598 match change.cmp(&0) {
599 Ordering::Less => format!("{change}").green().to_string(),
600 Ordering::Equal => change.to_string(),
601 Ordering::Greater => format!("{change}").red().to_string(),
602 }
603}
604
605fn within_tolerance(source_gas: u64, target_gas: u64, tolerance_pct: Option<u32>) -> bool {
609 if let Some(tolerance) = tolerance_pct {
610 let (hi, lo) = if source_gas > target_gas {
611 (source_gas, target_gas)
612 } else {
613 (target_gas, source_gas)
614 };
615 if hi == 0 {
616 return true;
618 }
619 let diff = (1. - (lo as f64 / hi as f64)) * 100.;
620 diff < tolerance as f64
621 } else {
622 source_gas == target_gas
623 }
624}
625
626#[cfg(test)]
627mod tests {
628 use super::*;
629
630 #[test]
631 fn test_tolerance() {
632 assert!(within_tolerance(100, 105, Some(5)));
633 assert!(within_tolerance(105, 100, Some(5)));
634 assert!(!within_tolerance(100, 106, Some(5)));
635 assert!(!within_tolerance(106, 100, Some(5)));
636 assert!(within_tolerance(100, 100, None));
637 assert!(within_tolerance(0, 0, Some(5)));
638 }
639
640 #[test]
641 fn can_parse_basic_gas_snapshot_entry() {
642 let s = "Test:deposit() (gas: 7222)";
643 let entry = GasSnapshotEntry::from_str(s).unwrap();
644 assert_eq!(
645 entry,
646 GasSnapshotEntry {
647 contract_name: "Test".to_string(),
648 signature: "deposit()".to_string(),
649 gas_used: TestKindReport::Unit { gas: 7222 }
650 }
651 );
652 }
653
654 #[test]
655 fn can_parse_fuzz_gas_snapshot_entry() {
656 let s = "Test:deposit() (runs: 256, μ: 100, ~:200)";
657 let entry = GasSnapshotEntry::from_str(s).unwrap();
658 assert_eq!(
659 entry,
660 GasSnapshotEntry {
661 contract_name: "Test".to_string(),
662 signature: "deposit()".to_string(),
663 gas_used: TestKindReport::Fuzz {
664 runs: 256,
665 median_gas: 200,
666 mean_gas: 100,
667 failed_corpus_replays: 0
668 }
669 }
670 );
671 }
672
673 #[test]
674 fn can_parse_invariant_gas_snapshot_entry() {
675 let s = "Test:deposit() (runs: 256, calls: 100, reverts: 200)";
676 let entry = GasSnapshotEntry::from_str(s).unwrap();
677 assert_eq!(
678 entry,
679 GasSnapshotEntry {
680 contract_name: "Test".to_string(),
681 signature: "deposit()".to_string(),
682 gas_used: TestKindReport::Invariant {
683 runs: 256,
684 calls: 100,
685 reverts: 200,
686 failed_corpus_replays: 0,
687 optimization_best_value: None,
688 }
689 }
690 );
691 }
692
693 #[test]
694 fn can_parse_invariant_gas_snapshot_entry2() {
695 let s = "ERC20Invariants:invariantBalanceSum() (runs: 256, calls: 3840, reverts: 2388)";
696 let entry = GasSnapshotEntry::from_str(s).unwrap();
697 assert_eq!(
698 entry,
699 GasSnapshotEntry {
700 contract_name: "ERC20Invariants".to_string(),
701 signature: "invariantBalanceSum()".to_string(),
702 gas_used: TestKindReport::Invariant {
703 runs: 256,
704 calls: 3840,
705 reverts: 2388,
706 failed_corpus_replays: 0,
707 optimization_best_value: None,
708 }
709 }
710 );
711 }
712
713 #[test]
714 fn rejects_overflowing_gas_snapshot_values() {
715 let u64_overflow = (u64::MAX as u128 + 1).to_string();
716 let usize_overflow = (usize::MAX as u128 + 1).to_string();
717 let cases = [
718 (
719 format!("Example:testFoo() (gas: {u64_overflow})"),
720 format!("invalid gas value `{u64_overflow}` in gas snapshot"),
721 ),
722 (
723 format!("Example:testFoo() (runs: {usize_overflow}, μ: 1, ~: 1)"),
724 format!("invalid runs value `{usize_overflow}` in gas snapshot"),
725 ),
726 (
727 format!("Example:testFoo() (runs: 1, μ: {u64_overflow}, ~: 1)"),
728 format!("invalid mean gas value `{u64_overflow}` in gas snapshot"),
729 ),
730 (
731 format!("Example:testFoo() (runs: 1, μ: 1, ~: {u64_overflow})"),
732 format!("invalid median gas value `{u64_overflow}` in gas snapshot"),
733 ),
734 (
735 format!("Example:invariantFoo() (runs: {usize_overflow}, calls: 1, reverts: 1)"),
736 format!("invalid runs value `{usize_overflow}` in gas snapshot"),
737 ),
738 (
739 format!("Example:invariantFoo() (runs: 1, calls: {usize_overflow}, reverts: 1)"),
740 format!("invalid calls value `{usize_overflow}` in gas snapshot"),
741 ),
742 (
743 format!("Example:invariantFoo() (runs: 1, calls: 1, reverts: {usize_overflow})"),
744 format!("invalid reverts value `{usize_overflow}` in gas snapshot"),
745 ),
746 ];
747
748 for (snapshot, expected) in cases {
749 assert_eq!(GasSnapshotEntry::from_str(&snapshot).unwrap_err(), expected);
750 }
751 }
752
753 #[test]
754 fn accepts_maximum_gas_snapshot_values() {
755 let unit = format!("Example:testFoo() (gas: {})", u64::MAX);
756 assert_eq!(
757 GasSnapshotEntry::from_str(&unit).unwrap().gas_used,
758 TestKindReport::Unit { gas: u64::MAX }
759 );
760
761 let fuzz =
762 format!("Example:testFoo() (runs: {}, μ: {}, ~: {})", usize::MAX, u64::MAX, u64::MAX);
763 assert_eq!(
764 GasSnapshotEntry::from_str(&fuzz).unwrap().gas_used,
765 TestKindReport::Fuzz {
766 runs: usize::MAX,
767 median_gas: u64::MAX,
768 mean_gas: u64::MAX,
769 failed_corpus_replays: 0,
770 }
771 );
772
773 let invariant = format!(
774 "Example:invariantFoo() (runs: {}, calls: {}, reverts: {})",
775 usize::MAX,
776 usize::MAX,
777 usize::MAX
778 );
779 assert_eq!(
780 GasSnapshotEntry::from_str(&invariant).unwrap().gas_used,
781 TestKindReport::Invariant {
782 runs: usize::MAX,
783 calls: usize::MAX,
784 reverts: usize::MAX,
785 failed_corpus_replays: 0,
786 optimization_best_value: None,
787 }
788 );
789 }
790
791 #[test]
792 fn reports_snapshot_path_and_line_for_invalid_values() {
793 let snapshot = tempfile::NamedTempFile::new().unwrap();
794 fs::write(
795 snapshot.path(),
796 "Example:testFoo() (gas: 1)\nExample:testBar() (gas: 18446744073709551616)",
797 )
798 .unwrap();
799
800 let error = read_gas_snapshot(snapshot.path()).unwrap_err();
801
802 assert_eq!(
803 format!("{error:#}"),
804 format!(
805 "failed to parse snapshot file \"{}\" at line 2: invalid gas value `18446744073709551616` in gas snapshot",
806 snapshot.path().display()
807 )
808 );
809 }
810}