diff --git a/crates/forge/src/cmd/coverage.rs b/crates/forge/src/cmd/coverage.rs index 90ef2968e5762..9ab9174a630ba 100644 --- a/crates/forge/src/cmd/coverage.rs +++ b/crates/forge/src/cmd/coverage.rs @@ -250,7 +250,6 @@ impl CoverageArgs { let filter = self.test.filter(&config)?; let outcome = self.test.run_tests(project_root, config, evm_opts, output, &filter, true).await?; - outcome.ensure_ok(false)?; let known_contracts = outcome.runner.as_ref().unwrap().known_contracts.clone(); @@ -300,6 +299,10 @@ impl CoverageArgs { // Output final reports. self.report(&report)?; + // Check for test failures after generating coverage report. + // This ensures coverage data is written even when tests fail. + outcome.ensure_ok(false)?; + Ok(()) } diff --git a/crates/forge/tests/cli/coverage.rs b/crates/forge/tests/cli/coverage.rs index 4e93d97eb624e..96c40723b3a8a 100644 --- a/crates/forge/tests/cli/coverage.rs +++ b/crates/forge/tests/cli/coverage.rs @@ -2203,3 +2203,66 @@ contract CounterTest is DSTest { ... "#]]); }); + +// Test that coverage files are written even when tests fail. +forgetest!(coverage_with_failing_tests, |prj, cmd| { + prj.insert_ds_test(); + prj.add_source( + "Counter.sol", + r#" +contract Counter { + uint256 public number; + + function setNumber(uint256 newNumber) public { + number = newNumber; + } + + function increment() public { + number++; + } +} + "#, + ); + + prj.add_source( + "CounterTest.sol", + r#" +import "./test.sol"; +import {Counter} from "./Counter.sol"; + +contract CounterTest is DSTest { + Counter public counter; + + function setUp() public { + counter = new Counter(); + counter.setNumber(0); + } + + function test_Increment() public { + counter.increment(); + assertEq(counter.number(), 1); + } + + function test_FailingTest() public { + counter.increment(); + // This assertion will fail + assertEq(counter.number(), 999); + } +} + "#, + ); + + // Run coverage - this should exit with error code 1 due to failing test, + // but the lcov file should still be written. + cmd.arg("coverage").args(["--report=lcov"]).assert_failure(); + + // Verify that the lcov.info file was created despite test failure + let lcov = prj.root().join("lcov.info"); + assert!(lcov.exists(), "lcov.info should be created even when tests fail"); + + // Verify the coverage data is valid and includes the counter contract + let lcov_content = std::fs::read_to_string(&lcov).unwrap(); + assert!(lcov_content.contains("SF:src/Counter.sol"), "Coverage should include Counter.sol"); + assert!(lcov_content.contains("FN:"), "Coverage should include function data"); + assert!(lcov_content.contains("DA:"), "Coverage should include line hit data"); +});