cirosantilli/nodejs/test_executables.js

nodejs/test_executables.js
#!/usr/bin/env node

const child_process = require("child_process")
const fs = require("fs")
const path = require("path")

function testLoop(topdir, fileOrDir, opts) {
  let dirents;
  let dir
  if (fs.lstatSync(fileOrDir).isDirectory()) {
    dir = fileOrDir
    dirents = fs.readdirSync(fileOrDir, { withFileTypes: true })
  } else {
    dirents = [{
      name: path.basename(fileOrDir),
      isDirectory: () => false,
    }]
    dir = path.dirname(fileOrDir)
  }
  for (let dirent of dirents.sort((a, b) => a.name.localeCompare(b.name))) {
    const curPath = path.join(dir, dirent.name)
    const relpath = path.relative(process.cwd(), curPath)
    if (!opts.skip.has(relpath)) {
      if (dirent.isDirectory()) {
        if (
          dirent.name !== 'node_modules' &&
          !testLoop(topdir, curPath, opts)
        ) {
          return false
        }
      } else {
        if (dirent.name !== 'test') {
          let isExec = true
          try {
            fs.accessSync(curPath, fs.constants.X_OK)
          } catch(e) {
            isExec = false
          }
          if (isExec) {
            let relpathArgsSkip = opts.argsSkip[relpath]
            if (relpathArgsSkip === undefined) {
              relpathArgsSkip = new Set()
            }
            let argsList;
            if (relpath in opts.args) {
              argsList = opts.args[relpath]
            } else if (opts.argsCommon !== undefined) {
              argsList = opts.argsCommon
            } else {
              argsList = [[]]
            }
            for (let i = 0; i < argsList.length; i++) {
              let args = argsList[i]
              if (!relpathArgsSkip.has(JSON.stringify(args))) {
                let cmd;
                let cmdPrint
                if (opts.pref) {
                  cmd = opts.pref[0]
                  args = opts.pref.slice(1).concat([relpath]).concat(args)
                  cmdPrint = cmd
                } else {
                  cmd = curPath
                  cmdPrint = relpath
                }
                console.log(([cmdPrint].concat(args)).join(' '))
                const out = child_process.spawnSync(cmd, args);
                if (out.status !== 0) {
                  let msg = `error:\n`;
                  if (out.stdout !== null) {
                    msg += `stdout: \n${out.stdout.toString('utf8')}\n`;
                  }
                  if (out.stderr !== null) {
                    msg += `stderr: \n${out.stderr.toString('utf8')}\n`;
                  }
                  console.log(msg)
                  return false
                }
              }
            }
          }
        }
      }
    }
  }
  return true
}

// Test all executables under a given directory recursively.
//
// opts:
//
// * argsCommon: string[][]. List of common command line arguments passed to all executables
//                     Each executable runs once with every given set of arguments.
// * args: {string: string[][]}. List of command line arguments passed to the chosen executable.
//                     That executable runs once with every given set of arguments given.
//                     This option overrides argsCommon for the selected paths.
// * argsSkip: {strig: string[][]}: don't run given executables with the given arg lists
//                     This can be used to skip certain argsCommon choices on certain executables.
// * argv: string[][]. process.argv of the ./test command. Elements:
//                     - 0: toplevel file or directory. If not given, use cwd.
// * pref: string[]. Prefix added to each command to run.
// * skip: string[]. List of paths to skip tests for, usually because they are failing,
//                   or because they are slow.
function test(opts = {}) {
  if (!('argsCommon' in opts)) {
    opts.argsCommon = [[]]
  }
  if (!('argsSkip' in opts)) {
    opts.argsSkip = {}
  }
  for (let relpath in opts.argsSkip) {
    const argsSkip = new Set()
    const argsSkipRelpath = opts.argsSkip[relpath]
    for (let args of argsSkipRelpath) {
      argsSkip.add(JSON.stringify(args))
    }
    opts.argsSkip[relpath] = argsSkip
  }
  if (!('argv' in opts)) {
    opts.argv = []
  }
  let toplevel;
  if (opts.argv.length > 2) {
    toplevel = path.resolve(opts.argv[2])
  } else {
    toplevel = process.cwd()
  }
  const ret = testLoop(toplevel, toplevel, opts) ? 0 : 1
  process.exit(ret);
}
exports.test = test

if (require.main === module) {
  test()
}