github.com/cgcardona/r-subnet-evm@v0.1.5/contracts/test/utils.ts (about)

     1  import { ethers } from "hardhat"
     2  import { CallOverrides } from "ethers"
     3  import assert from "assert"
     4  
     5  /*
     6   *
     7   * The `test` function is a wrapper around Mocha's `it` function. It provides a normalized framework for running the
     8   * majority of your test assertions inside of a smart-contract, using `DS-Test`.
     9   * The API can be used as follows (all of the examples are equivalent):
    10   * ```ts
    11   * test("<test_name>", "<contract_function_name>")
    12   * test("<test_name>", ["<contract_function_name>"])
    13   * test("<test_name>", { method: "<contract_function_name>", overrides: {}, shouldFail: false, debug: false })
    14   * test("<test_name>", [{ method: "<contract_function_name>", overrides: {}, shouldFail: false, debug: false }])
    15   * test("<test_name>", [{ method: "<contract_function_name>", shouldFail: false, debug: false }], {})
    16   * ```
    17   * Many contract functions can be called as a part of the same test:
    18   * ```ts
    19   * test("<test_name>", ["<step_fn1>", "<step_fn2>", "<step_fn3>"])
    20   * ```
    21   * Individual test functions can describe their own overrides with the `overrides` property.
    22   * If an object is passed in as the third argument to `test`, it will be used as the default overrides for all test
    23   * functions.
    24   * The following are equivalent:
    25   * ```ts
    26   * test("<test_name>", [{ method: "<contract_function_name>", overrides: { from: "0x123" } }])
    27   * test("<test_name>", [{ method: "<contract_function_name>" }], { from: "0x123" })
    28   * ```
    29   * In the above cases, the `from` override must be a signer.
    30   * The `shouldFail` property can be used to indicate that the test function should fail. This should be used sparingly
    31   * as it is not possible to match on the failure reason.
    32   * Furthermore, the `debug` property can be used to print any thrown errors when attempting to
    33   * send a transaction or while waiting for the transaction to be confirmed (the transaction is the smart contract call).
    34   * `debug` will also cause any parseable event logs to be printed that start with the `log_` prefix.
    35   * `DSTest` contracts have several options for emitting `log_` events.
    36   *
    37   */
    38  
    39  // Below are the types that help define all the different ways to call `test`
    40  type FnNameOrObject = string | string[] | MethodObject | MethodObject[]
    41  
    42  // Limit `from` property to be a `string` instead of `string | Promise<string>`
    43  type Overrides = CallOverrides & { from?: string }
    44  
    45  type MethodObject = { method: string, debug?: boolean, overrides?: Overrides, shouldFail?: boolean }
    46  
    47  // This type is after all default values have been applied
    48  type MethodWithDebugAndOverrides = MethodObject & { debug: boolean, overrides: Overrides, shouldFail: boolean }
    49  
    50  // `test` is used very similarly to `it` from Mocha
    51  export const test = (name, fnNameOrObject, overrides = {}) => it(name, buildTestFn(fnNameOrObject, overrides))
    52  // `test.only` is used very similarly to `it.only` from Mocha, it will isolate all tests marked with `test.only`
    53  test.only = (name, fnNameOrObject, overrides = {}) => it.only(name, buildTestFn(fnNameOrObject, overrides))
    54  // `test.debug` is used to apply `debug: true` to all DSTest contract method calls in the test
    55  test.debug = (name, fnNameOrObject, overrides = {}) => it.only(name, buildTestFn(fnNameOrObject, overrides, true))
    56  // `test.skip` is used very similarly to `it.skip` from Mocha, it will skip all tests marked with `test.skip`
    57  test.skip = (name, fnNameOrObject, overrides = {}) => it.skip(name, buildTestFn(fnNameOrObject, overrides))
    58  
    59  // `buildTestFn` is a higher-order function. It returns a function that can be used as the test function for `it`
    60  const buildTestFn = (fnNameOrObject: FnNameOrObject, overrides = {}, debug = false) => {
    61    // normalize the input to an array of objects
    62    const fnObjects: MethodWithDebugAndOverrides[] = (Array.isArray(fnNameOrObject) ? fnNameOrObject : [fnNameOrObject]).map(fnNameOrObject => {
    63      fnNameOrObject = typeof fnNameOrObject === 'string' ? { method: fnNameOrObject } : fnNameOrObject
    64      // assign all default values and overrides
    65      fnNameOrObject.overrides = Object.assign({}, overrides, fnNameOrObject.overrides ?? {})
    66      fnNameOrObject.debug = fnNameOrObject.debug ?? debug
    67      fnNameOrObject.shouldFail = fnNameOrObject.shouldFail ?? false
    68  
    69      return fnNameOrObject as MethodWithDebugAndOverrides
    70    })
    71  
    72    // only `step_` prefixed functions can be called on the `DSTest` contracts to clearly separate tests and helpers
    73    assert(fnObjects.every(({ method }) => method.startsWith('step_')), "Solidity test functions must be prefixed with 'step_'")
    74  
    75    // return the test function that will be used by `it`
    76    // this function must be defined with the `function` keyword so that `this` is bound to the Mocha context
    77    return async function () {
    78      // `Array.prototype.reduce` is used here to ensure that the test functions are called in order.
    79      // Each test function waits for its predecessor to complete before starting
    80      return fnObjects.reduce((p: Promise<undefined>, fn) => p.then(async () => {
    81        const contract = fn.overrides.from
    82          ? this.testContract.connect(await ethers.getSigner(fn.overrides.from))
    83          : this.testContract
    84        const tx = await contract[fn.method](fn.overrides).catch(err => {
    85          if (fn.shouldFail) {
    86            if (fn.debug) console.error(`smart contract call failed with error:\n${err}\n`)
    87  
    88            return { failed: true }
    89          }
    90  
    91          console.error("smart contract call failed with error:", err)
    92          throw err
    93        })
    94  
    95        // no more assertions necessary if the method-call should fail and did fail
    96        if (tx.failed && fn.shouldFail) return
    97  
    98        const txReceipt = await tx.wait().catch(err => {
    99          if (fn.debug) console.error(`tx failed with error:\n${err}\n`)
   100          return err.receipt
   101        })
   102  
   103        // `txReceipt.status` will be `0` if the transaction failed.
   104        // `contract.callStatic.failed()` will return `true` if any of the `DSTest` assertions failed.
   105        const failed = txReceipt.status === 0 ? true : await contract.callStatic.failed()
   106        if (fn.debug || failed) {
   107          console.log('')
   108  
   109          if (!txReceipt.events) console.warn('WARNING: No parseable events found in tx-receipt\n')
   110  
   111          // If `DSTest` assertions failed, the contract will emit logs describing the assertion failure(s).
   112          txReceipt
   113            .events
   114            ?.filter(event => fn.debug || event.event?.startsWith('log'))
   115            .map(event => event.args?.forEach(arg => console.log(arg)))
   116  
   117          console.log('')
   118        }
   119  
   120        if (fn.shouldFail) {
   121          assert(failed, `${fn.method} should have failed`)
   122        } else {
   123          assert(!failed, `${fn.method} failed`)
   124        }
   125      }), Promise.resolve())
   126    }
   127  }