forge-test-names.ts 3.16 KB
Newer Older
1
import fs from 'fs'
2 3
import path from 'path'
import { execSync } from 'child_process'
4

5 6 7 8 9 10
type Check = (parts: string[]) => boolean
type Checks = Array<{
  check: Check
  error: string
}>

11
/**
12
 * Series of function name checks.
13
 */
14
const checks: Checks = [
Mark Tyneway's avatar
Mark Tyneway committed
15 16 17 18 19 20
  {
    error: 'test name parts should be in camelCase',
    check: (parts: string[]): boolean => {
      return parts.every((part) => {
        return part[0] === part[0].toLowerCase()
      })
21
    },
Mark Tyneway's avatar
Mark Tyneway committed
22 23 24 25 26 27
  },
  {
    error:
      'test names should have either 3 or 4 parts, each separated by underscores',
    check: (parts: string[]): boolean => {
      return parts.length === 3 || parts.length === 4
28
    },
Mark Tyneway's avatar
Mark Tyneway committed
29 30 31 32 33
  },
  {
    error: 'test names should begin with "test", "testFuzz", or "testDiff"',
    check: (parts: string[]): boolean => {
      return ['test', 'testFuzz', 'testDiff'].includes(parts[0])
34
    },
Mark Tyneway's avatar
Mark Tyneway committed
35 36 37 38 39 40 41 42 43 44 45 46
  },
  {
    error:
      'test names should end with either "succeeds", "reverts", "fails", "works" or "benchmark[_num]"',
    check: (parts: string[]): boolean => {
      return (
        ['succeeds', 'reverts', 'fails', 'benchmark', 'works'].includes(
          parts[parts.length - 1]
        ) ||
        (parts[parts.length - 2] === 'benchmark' &&
          !isNaN(parseInt(parts[parts.length - 1], 10)))
      )
47
    },
Mark Tyneway's avatar
Mark Tyneway committed
48 49 50 51 52 53 54 55 56
  },
  {
    error:
      'failure tests should have 4 parts, third part should indicate the reason for failure',
    check: (parts: string[]): boolean => {
      return (
        parts.length === 4 ||
        !['reverts', 'fails'].includes(parts[parts.length - 1])
      )
57
    },
Mark Tyneway's avatar
Mark Tyneway committed
58 59
  },
]
60

61 62 63
/**
 * Script for checking that all test functions are named correctly.
 */
64
const main = async () => {
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
  const result = execSync('forge config --json')
  const config = JSON.parse(result.toString())
  const out = config.out || 'out'

  const paths = []

  const readFilesRecursively = (dir: string) => {
    const files = fs.readdirSync(dir)

    for (const file of files) {
      const filePath = path.join(dir, file)
      const fileStat = fs.statSync(filePath)

      if (fileStat.isDirectory()) {
        readFilesRecursively(filePath)
      } else {
        paths.push(filePath)
      }
    }
  }

  readFilesRecursively(out)

  console.log('Success:')
89
  const errors: string[] = []
90 91 92 93 94

  for (const filepath of paths) {
    const artifact = JSON.parse(fs.readFileSync(filepath, 'utf8'))

    let isTest = false
95
    for (const element of artifact.abi) {
96 97 98
      if (element.name === 'IS_TEST') {
        isTest = true
        break
99
      }
100 101 102 103 104 105 106 107 108
    }

    if (isTest) {
      let success = true
      for (const element of artifact.abi) {
        // Skip non-functions and functions that don't start with "test".
        if (element.type !== 'function' || !element.name.startsWith('test')) {
          continue
        }
109

110 111 112
        // Check the rest.
        for (const { check, error } of checks) {
          if (!check(element.name.split('_'))) {
113
            errors.push(`${filepath}#${element.name}: ${error}`)
114 115
            success = false
          }
116 117
        }
      }
118 119 120
      if (success) {
        console.log(` - ${path.parse(filepath).name}`)
      }
121
    }
122 123
  }

124
  if (errors.length > 0) {
125
    console.error(errors.join('\n'))
126
    process.exit(1)
127 128
  }
}
129 130

main()