diff --git a/testmodule/helpers.nim b/testmodule/helpers.nim new file mode 100644 index 0000000..3a577d2 --- /dev/null +++ b/testmodule/helpers.nim @@ -0,0 +1,53 @@ +import std/json +import std/strutils +import pkg/ethers + +proc revertReason*(e: ref EthersError): string = + try: + let json = parseJson(e.msg) + var msg = json{"message"}.getStr + const revertPrefixes = @[ + # hardhat + "Error: VM Exception while processing transaction: reverted with " & + "reason string ", + # ganache + "VM Exception while processing transaction: revert " + ] + for prefix in revertPrefixes.items: + msg = msg.replace(prefix) + msg = msg.replace("\'") + return msg + except JsonParsingError: + return "" + +template reverts*(body: untyped): untyped = + let asyncproc = proc(): Future[bool] {.async.} = + try: + body + return false + except EthersError: + return true + except CatchableError: + return false + waitFor asyncproc() + +template revertsWith*(reason: string, body: untyped): untyped = + let asyncproc = proc(): Future[bool] {.async.} = + try: + body + return false + except EthersError as e: + return reason == revertReason(e) + except CatchableError as e: + return false + waitFor asyncproc() + +template doesNotRevert*(body: untyped): untyped = + let asyncproc = proc(): Future[bool] {.async.} = + return not reverts(body) + waitFor asyncproc() + +template doesNotRevertWith*(reason: string, body: untyped): untyped = + let asyncproc = proc(): Future[bool] {.async.} = + return not revertsWith(reason, body) + waitFor asyncproc() \ No newline at end of file diff --git a/testmodule/testHelpers.nim b/testmodule/testHelpers.nim new file mode 100644 index 0000000..25aadef --- /dev/null +++ b/testmodule/testHelpers.nim @@ -0,0 +1,143 @@ +import std/json +import std/strformat +import pkg/asynctest +import pkg/chronos +import pkg/ethers +import ./hardhat +import ./helpers + +suite "Revert helpers": + + let revertReason = "revert reason" + let rpcResponse = %* { + "message": "Error: VM Exception while processing transaction: " & + fmt"reverted with reason string '{revertReason}'" + } + + test "can use block syntax async": + let ethCallAsync = proc() {.async.} = + raise newException(EthersError, $rpcResponse) + + check: + reverts: + await ethCallAsync() + + test "can use block syntax sync": + let ethCall = proc() = + raise newException(EthersError, $rpcResponse) + + check: + reverts: + ethCall() + + test "can use parameter syntax async": + let ethCallAsync = proc() {.async.} = + raise newException(EthersError, $rpcResponse) + + check: + reverts (await ethCallAsync()) + + test "can use parameter syntax sync": + let ethCall = proc() = + raise newException(EthersError, $rpcResponse) + + check: + reverts ethCall() + + test "successfully checks revert reason async": + let ethCallAsync = proc() {.async.} = + raise newException(EthersError, $rpcResponse) + + check: + revertsWith revertReason: + await ethCallAsync() + + test "successfully checks revert reason sync": + let ethCall = proc() = + raise newException(EthersError, $rpcResponse) + + check: + revertsWith revertReason: + ethCall() + + + test "correctly indicates there was no revert": + let ethCall = proc() = discard + + check: + doesNotRevert: + ethCall() + + test "only checks EthersErrors": + let ethCall = proc() = + raise newException(ValueError, $rpcResponse) + + check: + doesNotRevert: + ethCall() + + test "revertsWith is false when there is no revert": + let ethCall = proc() = discard + + check: + doesNotRevertWith revertReason: + ethCall() + + test "revertsWith is false when not an EthersError": + let ethCall = proc() = + raise newException(ValueError, $rpcResponse) + + check: + doesNotRevertWith revertReason: + ethCall() + + test "revertsWith is false when the revert reason doesn't match": + let ethCall = proc() = + raise newException(EthersError, "other reason") + + check: + doesNotRevertWith revertReason: + ethCall() + + test "revertsWith handles non-standard revert prefix": + let nonStdMsg = fmt"Provider VM Exception: reverted with {revertReason}" + let nonStdRpcResponse = %* { "message": nonStdMsg } + let ethCall = proc() = + raise newException(EthersError, $nonStdRpcResponse) + + check: + revertsWith nonStdMsg: + ethCall() + +type + TestHelpers* = ref object of Contract + +method revertsWith*(self: TestHelpers, + revertReason: string) {.base, contract, view.} + +suite "Revert helpers - current provider": + + var helpersContract: TestHelpers + var provider: JsonRpcProvider + var snapshot: JsonNode + var accounts: seq[Address] + let revertReason = "revert reason" + + setup: + provider = JsonRpcProvider.new("ws://127.0.0.1:8545") + snapshot = await provider.send("evm_snapshot") + accounts = await provider.listAccounts() + let deployment = readDeployment() + helpersContract = TestHelpers.new(!deployment.address(TestHelpers), provider) + + teardown: + discard await provider.send("evm_revert", @[snapshot]) + + test "revert prefix is emitted from current provider": + check: + revertsWith revertReason: + await helpersContract.revertsWith(revertReason) + + + + diff --git a/testnode/contracts/TestHelpers.sol b/testnode/contracts/TestHelpers.sol new file mode 100644 index 0000000..d4316b5 --- /dev/null +++ b/testnode/contracts/TestHelpers.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract TestHelpers { + + function revertsWith(string calldata revertReason) public pure { + require(false, revertReason); + } +} diff --git a/testnode/deploy/testhelpers.js b/testnode/deploy/testhelpers.js new file mode 100644 index 0000000..2d07aae --- /dev/null +++ b/testnode/deploy/testhelpers.js @@ -0,0 +1,6 @@ +module.exports = async ({deployments, getNamedAccounts}) => { + const { deployer } = await getNamedAccounts() + await deployments.deploy('TestHelpers', { from: deployer }) +} + +module.exports.tags = ["TestHelpers"];