-
Notifications
You must be signed in to change notification settings - Fork 222
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
94c973d
commit f7b9283
Showing
4 changed files
with
188 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
# Testing Precompiles | ||
|
||
If you can, put all of your test logic into DS-test tests. Prefix test functions with `test_`. There's also a `setUp` function that gets called before the test contract is deployed. The current best-practice is to re-deploy one test contract per `test` function called in `*.ts` test definitions. The `setUp` method should be called once, then `test_` functions passed in as the 2nd argument to `test("<description>", <test_function_name_OR_array_of_test_function_names>)` will be called in order. `test.only` and `test.skip` behave the same way as `it.only` and `it.skip`. There's also a `test.debug` that combines `test.only` with some extra event logging (you can use `emit log_string` to help debug Solidity test code). | ||
|
||
The `test` function is a wrapper around Mocha's `it` function. It provides a normalized framework for running the | ||
majority of your test assertions inside of a smart-contract, using `DS-Test`. | ||
The API can be used as follows: | ||
|
||
```js | ||
test("<test_name>", "<contract_function_name>"); | ||
test("<test_name>", ["<contract_function_name>"]); | ||
test("<test_name>", { | ||
method: "<contract_function_name>", | ||
overrides: {}, | ||
shouldFail: false, | ||
debug: false, | ||
}); | ||
test("<test_name>", [ | ||
{ | ||
method: "<contract_function_name>", | ||
overrides: {}, | ||
shouldFail: false, | ||
debug: false, | ||
}, | ||
]); | ||
test( | ||
"<test_name>", | ||
[{ method: "<contract_function_name>", shouldFail: false, debug: false }], | ||
{} | ||
); | ||
``` | ||
|
||
All of the above examples are equivalent. | ||
Many contract functions can be called as a part of the same test: | ||
|
||
```js | ||
test("<test_name>", [<test_fn1>, <test_fn2>, <test_fn3>]) | ||
``` | ||
|
||
Individual test functions can describe their own overrides with the `overrides` property. | ||
If an object is passed in as the third argument to `test`, it will be used as the default overrides for all test | ||
functions. | ||
The following are equivalent: | ||
|
||
```js | ||
test("<test_name>", [ | ||
{ method: "<contract_function_name>", overrides: { from: "0x123" } }, | ||
]); | ||
test("<test_name>", [{ method: "<contract_function_name>" }], { | ||
from: "0x123", | ||
}); | ||
``` | ||
|
||
In the above cases, the `from` override must be a signer. | ||
The `shouldFail` property can be used to indicate that the test function should fail. This should be used sparingly | ||
as it is not possible to match on the failure reason. | ||
Furthermore, the `debug` property can be used to print any thrown errors when attempting to | ||
send a transaction or while waiting for the transaction to be confirmed (the transaction is the smart contract call). | ||
`debug` will also cause any parseable event logs to be printed that start with the `log_` prefix. | ||
`DSTest` contracts have several options for emitting `log_` events. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
import { ethers } from "hardhat" | ||
const assert = require("assert") | ||
|
||
/* | ||
* | ||
* The `test` function is a wrapper around Mocha's `it` function. It provides a normalized framework for running the | ||
* majority of your test assertions inside of a smart-contract, using `DS-Test`. | ||
* The API can be used as follows: | ||
* ```js | ||
* test("<test_name>", "<contract_function_name>") | ||
* test("<test_name>", ["<contract_function_name>"]) | ||
* test("<test_name>", { method: "<contract_function_name>", overrides: {}, shouldFail: false, debug: false }) | ||
* test("<test_name>", [{ method: "<contract_function_name>", overrides: {}, shouldFail: false, debug: false }]) | ||
* test("<test_name>", [{ method: "<contract_function_name>", shouldFail: false, debug: false }], {}) | ||
* ``` | ||
* All of the above examples are equivalent. | ||
* Many contract functions can be called as a part of the same test: | ||
* ```js | ||
* test("<test_name>", [<test_fn1>, <test_fn2>, <test_fn3>]) | ||
* ``` | ||
* Individual test functions can describe their own overrides with the `overrides` property. | ||
* If an object is passed in as the third argument to `test`, it will be used as the default overrides for all test | ||
* functions. | ||
* The following are equivalent: | ||
* ```js | ||
* test("<test_name>", [{ method: "<contract_function_name>", overrides: { from: "0x123" } }]) | ||
* test("<test_name>", [{ method: "<contract_function_name>" }], { from: "0x123" }) | ||
* ``` | ||
* In the above cases, the `from` override must be a signer. | ||
* The `shouldFail` property can be used to indicate that the test function should fail. This should be used sparingly | ||
* as it is not possible to match on the failure reason. | ||
* Furthermore, the `debug` property can be used to print any thrown errors when attempting to | ||
* send a transaction or while waiting for the transaction to be confirmed (the transaction is the smart contract call). | ||
* `debug` will also cause any parseable event logs to be printed that start with the `log_` prefix. | ||
* `DSTest` contracts have several options for emitting `log_` events. | ||
* | ||
*/ | ||
|
||
// Below are the types that help define all the different ways to call `test` | ||
type FnNameOrObject = string | string[] | MethodObject | MethodObject[] | ||
type MethodObject = { method: string, debug?: boolean, overrides?: any, shouldFail?: boolean } | ||
|
||
// This type is after all default values have been applied | ||
type MethodWithDebugAndOverrides = MethodObject & { debug: boolean, overrides: any, shouldFail: boolean } | ||
|
||
// `test` is used very similarly to `it` from Mocha | ||
export const test = (name, fnNameOrObject, overrides = {}) => it(name, testFn(fnNameOrObject, overrides)) | ||
// `test.only` is used very similarly to `it.only` from Mocha, it will isolate all tests marked with `test.only` | ||
test.only = (name, fnNameOrObject, overrides = {}) => it.only(name, testFn(fnNameOrObject, overrides)) | ||
// `test.debug` is used to apply `debug: true` to all DSTest contract method calls in the test | ||
test.debug = (name, fnNameOrObject, overrides = {}) => it.only(name, testFn(fnNameOrObject, overrides, true)) | ||
// `test.skip` is used very similarly to `it.skip` from Mocha, it will skip all tests marked with `test.skip` | ||
test.skip = (name, fnNameOrObject, overrides = {}) => it.skip(name, testFn(fnNameOrObject, overrides)) | ||
|
||
// `testFn` is a higher-order function. It returns a function that can be used as the test function for `it` | ||
const testFn = (fnNameOrObject: FnNameOrObject, overrides = {}, debug = false) => { | ||
// normalize the input to an array of objects | ||
const fnObjects: MethodWithDebugAndOverrides[] = (Array.isArray(fnNameOrObject) ? fnNameOrObject : [fnNameOrObject]).map(fnNameOrObject => { | ||
fnNameOrObject = typeof fnNameOrObject === 'string' ? { method: fnNameOrObject } : fnNameOrObject | ||
// assign all default values and overrides | ||
fnNameOrObject.overrides = Object.assign({}, overrides, fnNameOrObject.overrides ?? {}) | ||
fnNameOrObject.debug = fnNameOrObject.debug ?? debug | ||
fnNameOrObject.shouldFail = fnNameOrObject.shouldFail ?? false | ||
|
||
return fnNameOrObject as MethodWithDebugAndOverrides | ||
}) | ||
|
||
// only `test_` prefixed functions can be called on the `DSTest` contracts to clearly separate tests and helpers | ||
assert(fnObjects.every(({ method }) => method.startsWith('test_')), "Solidity test functions must be prefixed with 'test_'") | ||
|
||
// return the test function that will be used by `it` | ||
// this function must be defined with the `function` keyword so that `this` is bound to the Mocha context | ||
return async function() { | ||
// `Array.prototype.reduce` is used here to ensure that the test functions are called in order. | ||
// Each test function waits for its predecessor to complete before starting | ||
return fnObjects.reduce((p: Promise<undefined>, fn) => p.then(async () => { | ||
const contract = fn.overrides.from | ||
? this.testContract.connect(await ethers.getSigner(fn.overrides.from)) | ||
: this.testContract | ||
const tx = await contract[fn.method](fn.overrides).catch(err => { | ||
if (fn.shouldFail) { | ||
if (fn.debug) console.error(`smart contract call failed with error:\n${err}\n`) | ||
|
||
return { failed: true } | ||
} | ||
|
||
console.error("smart contract call failed with error:", err) | ||
throw err | ||
}) | ||
|
||
// no more assertions necessary if the method-call should fail and did fail | ||
if (tx.failed && fn.shouldFail) return | ||
|
||
const txReceipt = await tx.wait().catch(err => { | ||
if (fn.debug) console.error(`tx failed with error:\n${err}\n`) | ||
return err.receipt | ||
}) | ||
|
||
// `txReceipt.status` will be `0` if the transaction failed. | ||
// `contract.callStatic.failed()` will return `true` if any of the `DSTest` assertions failed. | ||
const failed = txReceipt.status === 0 ? true : await contract.callStatic.failed() | ||
if (fn.debug || failed) { | ||
console.log('') | ||
|
||
if (!txReceipt.events) console.warn('WARNING: No parseable events found in tx-receipt\n') | ||
|
||
// If `DSTest` assertions failed, the contract will emit logs describing the assertion failure(s). | ||
txReceipt | ||
.events | ||
?.filter(event => fn.debug || event.event?.startsWith('log')) | ||
.map(event => event.args?.forEach(arg => console.log(arg))) | ||
|
||
console.log('') | ||
} | ||
|
||
if (fn.shouldFail) { | ||
assert(failed, `${fn.method} should have failed`) | ||
} else { | ||
assert(!failed, `${fn.method} failed`) | ||
} | ||
}), Promise.resolve()) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters