|
| 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 | + } |
0 commit comments