Skip to content

Commit 235fb75

Browse files
Fix Foundry test generation contract name collision (#1484)
* Fix Foundry test generation contract name collision * Add actual forge compilation test in Foundry test generation suite * Install foundry in CI
1 parent 797fe19 commit 235fb75

File tree

5 files changed

+91
-2
lines changed

5 files changed

+91
-2
lines changed

.github/workflows/ci.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,14 +249,17 @@ jobs:
249249
HOST_OS: ${{ runner.os }}
250250
SOLC_VER: ${{ matrix.solc }}
251251

252+
- name: Install Foundry
253+
uses: foundry-rs/foundry-toolchain@v1
254+
252255
- name: Download testsuite
253256
uses: actions/download-artifact@v7
254257
with:
255258
name: echidna-testsuite-${{ runner.os }}
256259

257260
- name: Test
258261
run: |
259-
export PATH="$PATH:$HOME/.local/bin"
262+
export PATH="$PATH:$HOME/.local/bin:$HOME/.foundry/bin"
260263
solc-select use ${{ matrix.solc }}
261264
chmod +x echidna-testsuite*
262265
./echidna-testsuite*

lib/Echidna/Output/Foundry.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ createTestData mContractName test =
4444
cName = fromMaybe "YourContract" mContractName
4545
in
4646
object
47-
[ "testName" .= ("Test" :: Text)
47+
[ "testName" .= ("FoundryTest" :: Text)
4848
, "contractName" .= cName
4949
, "actors" .= actors
5050
, "reproducer" .= repro

src/test/Spec.hs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import Tests.Config (configTests)
88
import Tests.Coverage (coverageTests)
99
import Tests.Dapptest (dapptestTests)
1010
import Tests.Encoding (encodingJSONTests)
11+
import Tests.FoundryTestGen (foundryTestGenTests)
1112
import Tests.Integration (integrationTests)
1213
import Tests.Optimization (optimizationTests)
1314
import Tests.Overflow (overflowTests)
@@ -32,6 +33,7 @@ main = withCurrentDirectory "./tests/solidity" . defaultMain $
3233
, researchTests
3334
, dapptestTests
3435
, encodingJSONTests
36+
, foundryTestGenTests
3537
, cheatTests
3638
, symbolicTests
3739
]

src/test/Tests/FoundryTestGen.hs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
module Tests.FoundryTestGen (foundryTestGenTests) where
2+
3+
import Test.Tasty (TestTree, testGroup)
4+
import Test.Tasty.HUnit (testCase, assertFailure)
5+
6+
import Control.Exception (catch, SomeException)
7+
import Data.Text (pack, unpack, replace)
8+
import qualified Data.Text.Lazy as TL
9+
import System.Directory (getTemporaryDirectory, removePathForcibly, findExecutable, copyFile)
10+
import System.Exit (ExitCode(..))
11+
import System.Process (readProcessWithExitCode)
12+
13+
import Echidna.Output.Foundry (foundryTest)
14+
import Echidna.Types.Test (EchidnaTest(..), TestType(..), TestValue(..), TestState(..))
15+
16+
foundryTestGenTests :: TestTree
17+
foundryTestGenTests = testGroup "Foundry test generation"
18+
[ testCase "compiles with forge" testForgeCompilation
19+
]
20+
21+
-- | Verify generated test compiles with forge.
22+
-- We use temp directories because we need to test the full forge workflow:
23+
-- forge init (for dependencies) + our generated test + forge build.
24+
testForgeCompilation :: IO ()
25+
testForgeCompilation = do
26+
forgeExe <- findExecutable "forge"
27+
case forgeExe of
28+
Nothing ->
29+
assertFailure "forge not found"
30+
Just _ -> do
31+
tmpBase <- getTemporaryDirectory
32+
let tmpDir = tmpBase ++ "/echidna-forge-test"
33+
34+
catch (removePathForcibly tmpDir) (\(_ :: SomeException) -> pure ())
35+
36+
-- Initialize project with forge.
37+
(code, _, err) <- readProcessWithExitCode "forge" ["init", tmpDir] ""
38+
if code /= ExitSuccess
39+
then assertFailure $ "forge init failed: " ++ err
40+
else do
41+
copyFile "foundry/FoundryTestTarget.sol" (tmpDir ++ "/src/FoundryTestTarget.sol")
42+
43+
-- Simulate user action: Replace the target contract with the actual
44+
-- contract instance and import it (add contract import after the
45+
-- forge-std one).
46+
let generated = TL.unpack $ foundryTest (Just "FoundryTestTarget") mkMinimalTest
47+
forgeStdImport = pack "import \"forge-std/Test.sol\";"
48+
contractImport = pack "import \"../src/FoundryTestTarget.sol\";"
49+
testWithImport = unpack $ replace forgeStdImport
50+
(forgeStdImport <> "\n" <> contractImport)
51+
(pack generated)
52+
53+
writeFile (tmpDir ++ "/test/Generated.t.sol") testWithImport
54+
55+
(buildCode, _, buildErr) <- readProcessWithExitCode "forge" ["build", "--root", tmpDir] ""
56+
57+
catch (removePathForcibly tmpDir) (\(_ :: SomeException) -> pure ())
58+
59+
if buildCode == ExitSuccess
60+
then pure ()
61+
else assertFailure $ "forge build failed: " ++ buildErr
62+
63+
mkMinimalTest :: EchidnaTest
64+
mkMinimalTest = EchidnaTest
65+
-- Foundry tests are only generated for solved/large tests.
66+
{ state = Large 0
67+
-- AssertionTest is required for Foundry test generation.
68+
, testType = AssertionTest False ("test", []) 0
69+
, value = BoolValue True
70+
-- Empty reproducer is sufficient for testing contract name generation.
71+
, reproducer = []
72+
-- These fields are not read by the output generator.
73+
, result = error "result not needed for Foundry output tests"
74+
, vm = Nothing
75+
, workerId = Nothing
76+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.0;
3+
4+
contract FoundryTestTarget {
5+
function test() public pure returns (bool) {
6+
return true;
7+
}
8+
}

0 commit comments

Comments
 (0)