Skip to main content

forge/
result.rs

1//! Test outcomes.
2
3use crate::{
4    MultiContractRunner,
5    fuzz::{BaseCounterExample, FuzzedCases},
6    gas_report::GasReport,
7};
8use alloy_primitives::{
9    Address, I256, Log, U256,
10    map::{AddressHashMap, HashMap},
11};
12use eyre::Report;
13use foundry_common::{get_contract_name, get_file_name, shell};
14use foundry_evm::{
15    core::Breakpoints,
16    coverage::HitMaps,
17    decode::SkipReason,
18    executors::{RawCallResult, invariant::InvariantMetrics},
19    fuzz::{CounterExample, FuzzCase, FuzzFixtures, FuzzTestResult},
20    traces::{CallTraceArena, CallTraceDecoder, TraceKind, Traces},
21};
22use serde::{Deserialize, Serialize};
23use std::{
24    collections::{BTreeMap, HashMap as Map},
25    fmt::{self, Write},
26    time::Duration,
27};
28use yansi::Paint;
29
30/// The aggregated result of a test run.
31#[derive(Clone, Debug)]
32pub struct TestOutcome {
33    /// The results of all test suites by their identifier (`path:contract_name`).
34    ///
35    /// Essentially `identifier => signature => result`.
36    pub results: BTreeMap<String, SuiteResult>,
37    /// Whether to allow test failures without failing the entire test run.
38    pub allow_failure: bool,
39    /// The decoder used to decode traces and logs.
40    ///
41    /// This is `None` if traces and logs were not decoded.
42    ///
43    /// Note that `Address` fields only contain the last executed test case's data.
44    pub last_run_decoder: Option<CallTraceDecoder>,
45    /// The gas report, if requested.
46    pub gas_report: Option<GasReport>,
47    /// The runner used to execute the tests.
48    pub runner: Option<MultiContractRunner>,
49    /// The fuzz seed used for the test run.
50    pub fuzz_seed: Option<U256>,
51}
52
53impl TestOutcome {
54    /// Creates a new test outcome with the given results.
55    pub fn new(
56        runner: Option<MultiContractRunner>,
57        results: BTreeMap<String, SuiteResult>,
58        allow_failure: bool,
59        fuzz_seed: Option<U256>,
60    ) -> Self {
61        Self { results, allow_failure, last_run_decoder: None, gas_report: None, runner, fuzz_seed }
62    }
63
64    /// Creates a new empty test outcome.
65    pub fn empty(runner: Option<MultiContractRunner>, allow_failure: bool) -> Self {
66        Self::new(runner, BTreeMap::new(), allow_failure, None)
67    }
68
69    /// Returns an iterator over all individual succeeding tests and their names.
70    pub fn successes(&self) -> impl Iterator<Item = (&String, &TestResult)> {
71        self.tests().filter(|(_, t)| t.status.is_success())
72    }
73
74    /// Returns an iterator over all individual skipped tests and their names.
75    pub fn skips(&self) -> impl Iterator<Item = (&String, &TestResult)> {
76        self.tests().filter(|(_, t)| t.status.is_skipped())
77    }
78
79    /// Returns an iterator over all individual failing tests and their names.
80    pub fn failures(&self) -> impl Iterator<Item = (&String, &TestResult)> {
81        self.tests().filter(|(_, t)| t.status.is_failure())
82    }
83
84    /// Returns an iterator over all individual tests and their names.
85    pub fn tests(&self) -> impl Iterator<Item = (&String, &TestResult)> {
86        self.results.values().flat_map(|suite| suite.tests())
87    }
88
89    /// Flattens the test outcome into a list of individual tests.
90    // TODO: Replace this with `tests` and make it return `TestRef<'_>`
91    pub fn into_tests_cloned(&self) -> impl Iterator<Item = SuiteTestResult> + '_ {
92        self.results
93            .iter()
94            .flat_map(|(file, suite)| {
95                suite
96                    .test_results
97                    .iter()
98                    .map(move |(sig, result)| (file.clone(), sig.clone(), result.clone()))
99            })
100            .map(|(artifact_id, signature, result)| SuiteTestResult {
101                artifact_id,
102                signature,
103                result,
104            })
105    }
106
107    /// Flattens the test outcome into a list of individual tests.
108    pub fn into_tests(self) -> impl Iterator<Item = SuiteTestResult> {
109        self.results
110            .into_iter()
111            .flat_map(|(file, suite)| {
112                suite.test_results.into_iter().map(move |t| (file.clone(), t))
113            })
114            .map(|(artifact_id, (signature, result))| SuiteTestResult {
115                artifact_id,
116                signature,
117                result,
118            })
119    }
120
121    /// Returns the number of tests that passed.
122    pub fn passed(&self) -> usize {
123        self.successes().count()
124    }
125
126    /// Returns the number of tests that were skipped.
127    pub fn skipped(&self) -> usize {
128        self.skips().count()
129    }
130
131    /// Returns the number of tests that failed.
132    pub fn failed(&self) -> usize {
133        self.failures().count()
134    }
135
136    /// Returns `true` if any fuzz or invariant test failed.
137    pub fn has_fuzz_failures(&self) -> bool {
138        self.failures().any(|(_, t)| t.kind.is_fuzz() || t.kind.is_invariant())
139    }
140
141    /// Sums up all the durations of all individual test suites.
142    ///
143    /// Note that this is not necessarily the wall clock time of the entire test run.
144    pub fn total_time(&self) -> Duration {
145        self.results.values().map(|suite| suite.duration).sum()
146    }
147
148    /// Formats the aggregated summary of all test suites into a string (for printing).
149    pub fn summary(&self, wall_clock_time: Duration) -> String {
150        let num_test_suites = self.results.len();
151        let suites = if num_test_suites == 1 { "suite" } else { "suites" };
152        let total_passed = self.passed();
153        let total_failed = self.failed();
154        let total_skipped = self.skipped();
155        let total_tests = total_passed + total_failed + total_skipped;
156        format!(
157            "\nRan {} test {} in {:.2?} ({:.2?} CPU time): {} tests passed, {} failed, {} skipped ({} total tests)",
158            num_test_suites,
159            suites,
160            wall_clock_time,
161            self.total_time(),
162            total_passed.green(),
163            total_failed.red(),
164            total_skipped.yellow(),
165            total_tests
166        )
167    }
168
169    /// Checks if there are any failures and failures are disallowed.
170    pub fn ensure_ok(&self, silent: bool) -> eyre::Result<()> {
171        let outcome = self;
172        let failures = outcome.failures().count();
173        if outcome.allow_failure || failures == 0 {
174            return Ok(());
175        }
176
177        if shell::is_quiet() || silent {
178            std::process::exit(1);
179        }
180
181        sh_println!("\nFailing tests:")?;
182        for (suite_name, suite) in &outcome.results {
183            let failed = suite.failed();
184            if failed == 0 {
185                continue;
186            }
187
188            let term = if failed > 1 { "tests" } else { "test" };
189            sh_println!("Encountered {failed} failing {term} in {suite_name}")?;
190            for (name, result) in suite.failures() {
191                sh_println!("{}", result.short_result(name))?;
192            }
193            sh_println!()?;
194        }
195        let successes = outcome.passed();
196        sh_println!(
197            "Encountered a total of {} failing tests, {} tests succeeded",
198            failures.to_string().red(),
199            successes.to_string().green()
200        )?;
201
202        // Show helpful hint for rerunning failed tests
203        let test_word = if failures == 1 { "test" } else { "tests" };
204        sh_println!(
205            "\nTip: Run {} to retry only the {} failed {}",
206            "`forge test --rerun`".cyan(),
207            failures,
208            test_word
209        )?;
210
211        // Print seed for fuzz/invariant test failures to enable reproduction.
212        if let Some(seed) = self.fuzz_seed
213            && outcome.has_fuzz_failures()
214        {
215            sh_println!(
216                "\nFuzz seed: {} (use {} to reproduce)",
217                format!("{seed:#x}").cyan(),
218                "`--fuzz-seed`".cyan()
219            )?;
220        }
221
222        std::process::exit(1);
223    }
224
225    /// Removes first test result, if any.
226    pub fn remove_first(&mut self) -> Option<(String, String, TestResult)> {
227        self.results.iter_mut().find_map(|(suite_name, suite)| {
228            if let Some(test_name) = suite.test_results.keys().next().cloned() {
229                let result = suite.test_results.remove(&test_name).unwrap();
230                Some((suite_name.clone(), test_name, result))
231            } else {
232                None
233            }
234        })
235    }
236}
237
238/// A set of test results for a single test suite, which is all the tests in a single contract.
239#[derive(Clone, Debug, Serialize)]
240pub struct SuiteResult {
241    /// Wall clock time it took to execute all tests in this suite.
242    #[serde(with = "foundry_common::serde_helpers::duration")]
243    pub duration: Duration,
244    /// Individual test results: `test fn signature -> TestResult`.
245    pub test_results: BTreeMap<String, TestResult>,
246    /// Generated warnings.
247    pub warnings: Vec<String>,
248}
249
250impl SuiteResult {
251    pub fn new(
252        duration: Duration,
253        test_results: BTreeMap<String, TestResult>,
254        mut warnings: Vec<String>,
255    ) -> Self {
256        // Add deprecated cheatcodes warning, if any of them used in current test suite.
257        let mut deprecated_cheatcodes = HashMap::new();
258        for test_result in test_results.values() {
259            deprecated_cheatcodes.extend(test_result.deprecated_cheatcodes.clone());
260        }
261        if !deprecated_cheatcodes.is_empty() {
262            let mut warning =
263                "the following cheatcode(s) are deprecated and will be removed in future versions:"
264                    .to_string();
265            for (cheatcode, reason) in deprecated_cheatcodes {
266                write!(warning, "\n  {cheatcode}").unwrap();
267                if let Some(reason) = reason {
268                    write!(warning, ": {reason}").unwrap();
269                }
270            }
271            warnings.push(warning);
272        }
273
274        Self { duration, test_results, warnings }
275    }
276
277    /// Returns an iterator over all individual succeeding tests and their names.
278    pub fn successes(&self) -> impl Iterator<Item = (&String, &TestResult)> {
279        self.tests().filter(|(_, t)| t.status.is_success())
280    }
281
282    /// Returns an iterator over all individual skipped tests and their names.
283    pub fn skips(&self) -> impl Iterator<Item = (&String, &TestResult)> {
284        self.tests().filter(|(_, t)| t.status.is_skipped())
285    }
286
287    /// Returns an iterator over all individual failing tests and their names.
288    pub fn failures(&self) -> impl Iterator<Item = (&String, &TestResult)> {
289        self.tests().filter(|(_, t)| t.status.is_failure())
290    }
291
292    /// Returns the number of tests that passed.
293    pub fn passed(&self) -> usize {
294        self.successes().count()
295    }
296
297    /// Returns the number of tests that were skipped.
298    pub fn skipped(&self) -> usize {
299        self.skips().count()
300    }
301
302    /// Returns the number of tests that failed.
303    pub fn failed(&self) -> usize {
304        self.failures().count()
305    }
306
307    /// Iterator over all tests and their names
308    pub fn tests(&self) -> impl Iterator<Item = (&String, &TestResult)> {
309        self.test_results.iter()
310    }
311
312    /// Whether this test suite is empty.
313    pub fn is_empty(&self) -> bool {
314        self.test_results.is_empty()
315    }
316
317    /// The number of tests in this test suite.
318    pub fn len(&self) -> usize {
319        self.test_results.len()
320    }
321
322    /// Sums up all the durations of all individual tests in this suite.
323    ///
324    /// Note that this is not necessarily the wall clock time of the entire test suite.
325    pub fn total_time(&self) -> Duration {
326        self.test_results.values().map(|result| result.duration).sum()
327    }
328
329    /// Returns the summary of a single test suite.
330    pub fn summary(&self) -> String {
331        let failed = self.failed();
332        let result = if failed == 0 { "ok".green() } else { "FAILED".red() };
333        format!(
334            "Suite result: {}. {} passed; {} failed; {} skipped; finished in {:.2?} ({:.2?} CPU time)",
335            result,
336            self.passed().green(),
337            failed.red(),
338            self.skipped().yellow(),
339            self.duration,
340            self.total_time(),
341        )
342    }
343}
344
345/// The result of a single test in a test suite.
346///
347/// This is flattened from a [`TestOutcome`].
348#[derive(Clone, Debug)]
349pub struct SuiteTestResult {
350    /// The identifier of the artifact/contract in the form:
351    /// `<artifact file name>:<contract name>`.
352    pub artifact_id: String,
353    /// The function signature of the Solidity test.
354    pub signature: String,
355    /// The result of the executed test.
356    pub result: TestResult,
357}
358
359impl SuiteTestResult {
360    /// Returns the gas used by the test.
361    pub fn gas_used(&self) -> u64 {
362        self.result.kind.report().gas()
363    }
364
365    /// Returns the contract name of the artifact ID.
366    pub fn contract_name(&self) -> &str {
367        get_contract_name(&self.artifact_id)
368    }
369
370    /// Returns the file name of the artifact ID.
371    pub fn file_name(&self) -> &str {
372        get_file_name(&self.artifact_id)
373    }
374}
375
376/// The status of a test.
377#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
378pub enum TestStatus {
379    Success,
380    #[default]
381    Failure,
382    Skipped,
383}
384
385impl TestStatus {
386    /// Returns `true` if the test was successful.
387    #[inline]
388    pub fn is_success(self) -> bool {
389        matches!(self, Self::Success)
390    }
391
392    /// Returns `true` if the test failed.
393    #[inline]
394    pub fn is_failure(self) -> bool {
395        matches!(self, Self::Failure)
396    }
397
398    /// Returns `true` if the test was skipped.
399    #[inline]
400    pub fn is_skipped(self) -> bool {
401        matches!(self, Self::Skipped)
402    }
403}
404
405/// The result of an executed test.
406#[derive(Clone, Debug, Default, Serialize, Deserialize)]
407pub struct TestResult {
408    /// The test status, indicating whether the test case succeeded, failed, or was marked as
409    /// skipped. This means that the transaction executed properly, the test was marked as
410    /// skipped with vm.skip(), or that there was a revert and that the test was expected to
411    /// fail (prefixed with `testFail`)
412    pub status: TestStatus,
413
414    /// If there was a revert, this field will be populated. Note that the test can
415    /// still be successful (i.e self.success == true) when it's expected to fail.
416    pub reason: Option<String>,
417
418    /// Minimal reproduction test case for failing test
419    pub counterexample: Option<CounterExample>,
420
421    /// Any captured & parsed as strings logs along the test's execution which should
422    /// be printed to the user.
423    pub logs: Vec<Log>,
424
425    /// The decoded DSTest logging events and Hardhat's `console.log` from [logs](Self::logs).
426    /// Used for json output.
427    pub decoded_logs: Vec<String>,
428
429    /// What kind of test this was
430    pub kind: TestKind,
431
432    /// Traces
433    pub traces: Traces,
434
435    /// Additional traces to use for gas report.
436    ///
437    /// These are cleared after the gas report is analyzed.
438    #[serde(skip)]
439    pub gas_report_traces: Vec<Vec<CallTraceArena>>,
440
441    /// Raw line coverage info
442    #[serde(skip)]
443    pub line_coverage: Option<HitMaps>,
444
445    /// Labeled addresses
446    #[serde(rename = "labeled_addresses")] // Backwards compatibility.
447    pub labels: AddressHashMap<String>,
448
449    #[serde(with = "foundry_common::serde_helpers::duration")]
450    pub duration: Duration,
451
452    /// pc breakpoint char map
453    pub breakpoints: Breakpoints,
454
455    /// Any captured gas snapshots along the test's execution which should be accumulated.
456    pub gas_snapshots: BTreeMap<String, BTreeMap<String, String>>,
457
458    /// Deprecated cheatcodes (mapped to their replacements, if any) used in current test.
459    #[serde(skip)]
460    pub deprecated_cheatcodes: HashMap<&'static str, Option<&'static str>>,
461}
462
463impl fmt::Display for TestResult {
464    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
465        match self.status {
466            TestStatus::Success => {
467                // For optimization mode, show the best example sequence in green.
468                if let Some(CounterExample::Sequence(original, sequence)) = &self.counterexample {
469                    let mut s = String::from("[PASS]");
470                    s.push_str(
471                        format!(
472                            "\n\t[Best sequence] (original: {original}, shrunk: {})\n",
473                            sequence.len()
474                        )
475                        .as_str(),
476                    );
477                    for ex in sequence {
478                        writeln!(s, "{ex}").unwrap();
479                    }
480                    s.green().wrap().fmt(f)
481                } else {
482                    "[PASS]".green().fmt(f)
483                }
484            }
485            TestStatus::Skipped => {
486                let mut s = String::from("[SKIP");
487                if let Some(reason) = &self.reason {
488                    write!(s, ": {reason}").unwrap();
489                }
490                s.push(']');
491                s.yellow().fmt(f)
492            }
493            TestStatus::Failure => {
494                let mut s = String::from("[FAIL");
495                if self.reason.is_some() || self.counterexample.is_some() {
496                    if let Some(reason) = &self.reason {
497                        write!(s, ": {reason}").unwrap();
498                    }
499
500                    if let Some(counterexample) = &self.counterexample {
501                        match counterexample {
502                            CounterExample::Single(ex) => {
503                                write!(s, "; counterexample: {ex}]").unwrap();
504                            }
505                            CounterExample::Sequence(original, sequence) => {
506                                s.push_str(
507                                    format!(
508                                        "]\n\t[Sequence] (original: {original}, shrunk: {})\n",
509                                        sequence.len()
510                                    )
511                                    .as_str(),
512                                );
513                                for ex in sequence {
514                                    writeln!(s, "{ex}").unwrap();
515                                }
516                            }
517                        }
518                    } else {
519                        s.push(']');
520                    }
521                } else {
522                    s.push(']');
523                }
524                s.red().wrap().fmt(f)
525            }
526        }
527    }
528}
529
530macro_rules! extend {
531    ($a:expr, $b:expr, $trace_kind:expr) => {
532        $a.logs.extend($b.logs);
533        $a.labels.extend($b.labels);
534        $a.traces.extend($b.traces.map(|traces| ($trace_kind, traces)));
535        $a.merge_coverages($b.line_coverage);
536    };
537}
538
539impl TestResult {
540    /// Creates a new test result starting from test setup results.
541    pub fn new(setup: &TestSetup) -> Self {
542        Self {
543            labels: setup.labels.clone(),
544            logs: setup.logs.clone(),
545            traces: setup.traces.clone(),
546            line_coverage: setup.coverage.clone(),
547            ..Default::default()
548        }
549    }
550
551    /// Creates a failed test result with given reason.
552    pub fn fail(reason: String) -> Self {
553        Self { status: TestStatus::Failure, reason: Some(reason), ..Default::default() }
554    }
555
556    /// Creates a test setup result.
557    pub fn setup_result(setup: TestSetup) -> Self {
558        let TestSetup {
559            address: _,
560            fuzz_fixtures: _,
561            logs,
562            labels,
563            traces,
564            coverage,
565            deployed_libs: _,
566            reason,
567            skipped,
568            deployment_failure: _,
569        } = setup;
570        Self {
571            status: if skipped { TestStatus::Skipped } else { TestStatus::Failure },
572            reason,
573            logs,
574            traces,
575            line_coverage: coverage,
576            labels,
577            ..Default::default()
578        }
579    }
580
581    /// Returns the skipped result for single test (used in skipped fuzz test too).
582    pub fn single_skip(&mut self, reason: SkipReason) {
583        self.status = TestStatus::Skipped;
584        self.reason = reason.0;
585    }
586
587    /// Returns the failed result with reason for single test.
588    pub fn single_fail(&mut self, reason: Option<String>) {
589        self.status = TestStatus::Failure;
590        self.reason = reason;
591    }
592
593    /// Returns the result for single test. Merges execution results (logs, labeled addresses,
594    /// traces and coverages) in initial setup results.
595    pub fn single_result(
596        &mut self,
597        success: bool,
598        reason: Option<String>,
599        raw_call_result: RawCallResult,
600    ) {
601        self.kind = TestKind::Unit {
602            gas: raw_call_result.gas_used.saturating_sub(raw_call_result.stipend),
603        };
604
605        extend!(self, raw_call_result, TraceKind::Execution);
606
607        self.status = match success {
608            true => TestStatus::Success,
609            false => TestStatus::Failure,
610        };
611        self.reason = reason;
612        self.duration = Duration::default();
613        self.gas_report_traces = Vec::new();
614
615        if let Some(cheatcodes) = raw_call_result.cheatcodes {
616            self.breakpoints = cheatcodes.breakpoints;
617            self.gas_snapshots = cheatcodes.gas_snapshots;
618            self.deprecated_cheatcodes = cheatcodes.deprecated;
619        }
620    }
621
622    /// Returns the result for a fuzzed test. Merges fuzz execution results (logs, labeled
623    /// addresses, traces and coverages) in initial setup results.
624    pub fn fuzz_result(&mut self, result: FuzzTestResult) {
625        self.kind = TestKind::Fuzz {
626            median_gas: result.median_gas(false),
627            mean_gas: result.mean_gas(false),
628            first_case: result.first_case,
629            runs: result.gas_by_case.len(),
630            failed_corpus_replays: result.failed_corpus_replays,
631        };
632
633        // Record logs, labels, traces and merge coverages.
634        extend!(self, result, TraceKind::Execution);
635
636        self.status = if result.skipped {
637            TestStatus::Skipped
638        } else if result.success {
639            TestStatus::Success
640        } else {
641            TestStatus::Failure
642        };
643        self.reason = result.reason;
644        self.counterexample = result.counterexample;
645        self.duration = Duration::default();
646        self.gas_report_traces = result.gas_report_traces.into_iter().map(|t| vec![t]).collect();
647        self.breakpoints = result.breakpoints.unwrap_or_default();
648        self.deprecated_cheatcodes = result.deprecated_cheatcodes;
649    }
650
651    /// Returns the fail result for fuzz test setup.
652    pub fn fuzz_setup_fail(&mut self, e: Report) {
653        self.kind = TestKind::Fuzz {
654            first_case: Default::default(),
655            runs: 0,
656            mean_gas: 0,
657            median_gas: 0,
658            failed_corpus_replays: 0,
659        };
660        self.status = TestStatus::Failure;
661        debug!(?e, "failed to set up fuzz testing environment");
662        self.reason = Some(format!("failed to set up fuzz testing environment: {e}"));
663    }
664
665    /// Returns the skipped result for invariant test.
666    pub fn invariant_skip(&mut self, reason: SkipReason) {
667        self.kind = TestKind::Invariant {
668            runs: 1,
669            calls: 1,
670            reverts: 1,
671            metrics: HashMap::default(),
672            failed_corpus_replays: 0,
673            optimization_best_value: None,
674        };
675        self.status = TestStatus::Skipped;
676        self.reason = reason.0;
677    }
678
679    /// Returns the fail result for replayed invariant test.
680    pub fn invariant_replay_fail(
681        &mut self,
682        replayed_entirely: bool,
683        invariant_name: &String,
684        call_sequence: Vec<BaseCounterExample>,
685    ) {
686        self.kind = TestKind::Invariant {
687            runs: 1,
688            calls: 1,
689            reverts: 1,
690            metrics: HashMap::default(),
691            failed_corpus_replays: 0,
692            optimization_best_value: None,
693        };
694        self.status = TestStatus::Failure;
695        self.reason = if replayed_entirely {
696            Some(format!("{invariant_name} replay failure"))
697        } else {
698            Some(format!("{invariant_name} persisted failure revert"))
699        };
700        self.counterexample = Some(CounterExample::Sequence(call_sequence.len(), call_sequence));
701    }
702
703    /// Returns the fail result for invariant test setup.
704    pub fn invariant_setup_fail(&mut self, e: Report) {
705        self.kind = TestKind::Invariant {
706            runs: 0,
707            calls: 0,
708            reverts: 0,
709            metrics: HashMap::default(),
710            failed_corpus_replays: 0,
711            optimization_best_value: None,
712        };
713        self.status = TestStatus::Failure;
714        self.reason = Some(format!("failed to set up invariant testing environment: {e}"));
715    }
716
717    /// Returns the invariant test result.
718    #[expect(clippy::too_many_arguments)]
719    pub fn invariant_result(
720        &mut self,
721        gas_report_traces: Vec<Vec<CallTraceArena>>,
722        success: bool,
723        reason: Option<String>,
724        counterexample: Option<CounterExample>,
725        cases: Vec<FuzzedCases>,
726        reverts: usize,
727        metrics: Map<String, InvariantMetrics>,
728        failed_corpus_replays: usize,
729        optimization_best_value: Option<I256>,
730    ) {
731        self.kind = TestKind::Invariant {
732            runs: cases.len(),
733            calls: cases.iter().map(|sequence| sequence.cases().len()).sum(),
734            reverts,
735            metrics,
736            failed_corpus_replays,
737            optimization_best_value,
738        };
739        // For optimization mode (Some value), always succeed. For check mode (None), use success.
740        self.status = if optimization_best_value.is_some() || success {
741            TestStatus::Success
742        } else {
743            TestStatus::Failure
744        };
745        self.reason = reason;
746        self.counterexample = counterexample;
747        self.gas_report_traces = gas_report_traces;
748    }
749
750    /// Returns the result for a table test. Merges table test execution results (logs, labeled
751    /// addresses, traces and coverages) in initial setup results.
752    pub fn table_result(&mut self, result: FuzzTestResult) {
753        self.kind = TestKind::Table {
754            median_gas: result.median_gas(false),
755            mean_gas: result.mean_gas(false),
756            runs: result.gas_by_case.len(),
757        };
758
759        // Record logs, labels, traces and merge coverages.
760        extend!(self, result, TraceKind::Execution);
761
762        self.status = if result.skipped {
763            TestStatus::Skipped
764        } else if result.success {
765            TestStatus::Success
766        } else {
767            TestStatus::Failure
768        };
769        self.reason = result.reason;
770        self.counterexample = result.counterexample;
771        self.duration = Duration::default();
772        self.gas_report_traces = result.gas_report_traces.into_iter().map(|t| vec![t]).collect();
773        self.breakpoints = result.breakpoints.unwrap_or_default();
774        self.deprecated_cheatcodes = result.deprecated_cheatcodes;
775    }
776
777    /// Returns `true` if this is the result of a fuzz test
778    pub fn is_fuzz(&self) -> bool {
779        matches!(self.kind, TestKind::Fuzz { .. })
780    }
781
782    /// Formats the test result into a string (for printing).
783    pub fn short_result(&self, name: &str) -> String {
784        format!("{self} {name} {}", self.kind.report())
785    }
786
787    /// Merges the given raw call result into `self`.
788    pub fn extend(&mut self, call_result: RawCallResult) {
789        extend!(self, call_result, TraceKind::Execution);
790    }
791
792    /// Merges the given coverage result into `self`.
793    pub fn merge_coverages(&mut self, other_coverage: Option<HitMaps>) {
794        HitMaps::merge_opt(&mut self.line_coverage, other_coverage);
795    }
796}
797
798/// Data report by a test.
799#[derive(Clone, Debug, PartialEq, Eq)]
800pub enum TestKindReport {
801    Unit {
802        gas: u64,
803    },
804    Fuzz {
805        runs: usize,
806        mean_gas: u64,
807        median_gas: u64,
808        failed_corpus_replays: usize,
809    },
810    Invariant {
811        runs: usize,
812        calls: usize,
813        reverts: usize,
814        metrics: Map<String, InvariantMetrics>,
815        failed_corpus_replays: usize,
816        /// For optimization mode (int256 return): the best value achieved. None = check mode.
817        optimization_best_value: Option<I256>,
818    },
819    Table {
820        runs: usize,
821        mean_gas: u64,
822        median_gas: u64,
823    },
824}
825
826impl fmt::Display for TestKindReport {
827    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
828        match self {
829            Self::Unit { gas } => {
830                write!(f, "(gas: {gas})")
831            }
832            Self::Fuzz { runs, mean_gas, median_gas, failed_corpus_replays } => {
833                if *failed_corpus_replays != 0 {
834                    write!(
835                        f,
836                        "(runs: {runs}, μ: {mean_gas}, ~: {median_gas}, failed corpus replays: {failed_corpus_replays})"
837                    )
838                } else {
839                    write!(f, "(runs: {runs}, μ: {mean_gas}, ~: {median_gas})")
840                }
841            }
842            Self::Invariant {
843                runs,
844                calls,
845                reverts,
846                metrics: _,
847                failed_corpus_replays,
848                optimization_best_value,
849            } => {
850                // If optimization_best_value is Some, this is optimization mode.
851                if let Some(best_value) = optimization_best_value {
852                    write!(f, "(best: {best_value}, runs: {runs}, calls: {calls})")
853                } else if *failed_corpus_replays != 0 {
854                    write!(
855                        f,
856                        "(runs: {runs}, calls: {calls}, reverts: {reverts}, failed corpus replays: {failed_corpus_replays})"
857                    )
858                } else {
859                    write!(f, "(runs: {runs}, calls: {calls}, reverts: {reverts})")
860                }
861            }
862            Self::Table { runs, mean_gas, median_gas } => {
863                write!(f, "(runs: {runs}, μ: {mean_gas}, ~: {median_gas})")
864            }
865        }
866    }
867}
868
869impl TestKindReport {
870    /// Returns the main gas value to compare against
871    pub fn gas(&self) -> u64 {
872        match *self {
873            Self::Unit { gas } => gas,
874            // We use the median for comparisons
875            Self::Fuzz { median_gas, .. } | Self::Table { median_gas, .. } => median_gas,
876            // We return 0 since it's not applicable
877            Self::Invariant { .. } => 0,
878        }
879    }
880}
881
882/// Various types of tests
883#[derive(Clone, Debug, Serialize, Deserialize)]
884pub enum TestKind {
885    /// A unit test.
886    Unit { gas: u64 },
887    /// A fuzz test.
888    Fuzz {
889        /// we keep this for the debugger
890        first_case: FuzzCase,
891        runs: usize,
892        mean_gas: u64,
893        median_gas: u64,
894        failed_corpus_replays: usize,
895    },
896    /// An invariant test.
897    Invariant {
898        runs: usize,
899        calls: usize,
900        reverts: usize,
901        metrics: Map<String, InvariantMetrics>,
902        failed_corpus_replays: usize,
903        /// For optimization mode (int256 return): the best value achieved. None = check mode.
904        optimization_best_value: Option<I256>,
905    },
906    /// A table test.
907    Table { runs: usize, mean_gas: u64, median_gas: u64 },
908}
909
910impl Default for TestKind {
911    fn default() -> Self {
912        Self::Unit { gas: 0 }
913    }
914}
915
916impl TestKind {
917    /// Returns `true` if this is a fuzz test.
918    pub fn is_fuzz(&self) -> bool {
919        matches!(self, Self::Fuzz { .. })
920    }
921
922    /// Returns `true` if this is an invariant test.
923    pub fn is_invariant(&self) -> bool {
924        matches!(self, Self::Invariant { .. })
925    }
926
927    /// The gas consumed by this test
928    pub fn report(&self) -> TestKindReport {
929        match self {
930            Self::Unit { gas } => TestKindReport::Unit { gas: *gas },
931            Self::Fuzz { first_case: _, runs, mean_gas, median_gas, failed_corpus_replays } => {
932                TestKindReport::Fuzz {
933                    runs: *runs,
934                    mean_gas: *mean_gas,
935                    median_gas: *median_gas,
936                    failed_corpus_replays: *failed_corpus_replays,
937                }
938            }
939            Self::Invariant {
940                runs,
941                calls,
942                reverts,
943                metrics: _,
944                failed_corpus_replays,
945                optimization_best_value,
946            } => TestKindReport::Invariant {
947                runs: *runs,
948                calls: *calls,
949                reverts: *reverts,
950                metrics: HashMap::default(),
951                failed_corpus_replays: *failed_corpus_replays,
952                optimization_best_value: *optimization_best_value,
953            },
954            Self::Table { runs, mean_gas, median_gas } => {
955                TestKindReport::Table { runs: *runs, mean_gas: *mean_gas, median_gas: *median_gas }
956            }
957        }
958    }
959}
960
961/// The result of a test setup.
962///
963/// Includes the deployment of the required libraries and the test contract itself, and the call to
964/// the `setUp()` function.
965#[derive(Clone, Debug, Default)]
966pub struct TestSetup {
967    /// The address at which the test contract was deployed.
968    pub address: Address,
969    /// Defined fuzz test fixtures.
970    pub fuzz_fixtures: FuzzFixtures,
971
972    /// The logs emitted during setup.
973    pub logs: Vec<Log>,
974    /// Addresses labeled during setup.
975    pub labels: AddressHashMap<String>,
976    /// Call traces of the setup.
977    pub traces: Traces,
978    /// Coverage info during setup.
979    pub coverage: Option<HitMaps>,
980    /// Addresses of external libraries deployed during setup.
981    pub deployed_libs: Vec<Address>,
982
983    /// The reason the setup failed, if it did.
984    pub reason: Option<String>,
985    /// Whether setup and entire test suite is skipped.
986    pub skipped: bool,
987    /// Whether the test failed to deploy.
988    pub deployment_failure: bool,
989}
990
991impl TestSetup {
992    pub fn failed(reason: String) -> Self {
993        Self { reason: Some(reason), ..Default::default() }
994    }
995
996    pub fn skipped(reason: String) -> Self {
997        Self { reason: Some(reason), skipped: true, ..Default::default() }
998    }
999
1000    pub fn extend(&mut self, raw: RawCallResult, trace_kind: TraceKind) {
1001        extend!(self, raw, trace_kind);
1002    }
1003
1004    pub fn merge_coverages(&mut self, other_coverage: Option<HitMaps>) {
1005        HitMaps::merge_opt(&mut self.coverage, other_coverage);
1006    }
1007}