Commit e5d56e2

Anton Golub <mailbox@antongolub.ru>
2022-04-15 13:33:32
test: tweak up test-utils (#360)
1 parent ab7db8f
.github/workflows/test.yml
@@ -23,5 +23,6 @@ jobs:
         node-version: ${{ matrix.node-version }}
     - run: npm i
     - run: npm test
+      timeout-minutes: 1
       env:
         FORCE_COLOR: 3
test/experimental.test.mjs
@@ -13,12 +13,12 @@
 // limitations under the License.
 
 import {echo, retry, startSpinner, withTimeout} from '../src/experimental.mjs'
-import {assert, test as t} from './test-utils.mjs'
+import {assert, testFactory} from './test-utils.mjs'
 import chalk from 'chalk'
 
-const test = t.bind(null, 'experimental')
+const test = testFactory('experimental', import.meta)
 
-if (test('Retry works')) {
+test('Retry works', async () => {
   let exitCode = 0
   let now = Date.now()
   try {
@@ -28,9 +28,9 @@ if (test('Retry works')) {
   }
   assert.equal(exitCode, 123)
   assert(Date.now() >= now + 50 * (5 - 1))
-}
+})
 
-if (test('withTimeout works')) {
+test('withTimeout works', async () => {
   let exitCode = 0
   let signal
   try {
@@ -44,17 +44,17 @@ if (test('withTimeout works')) {
 
   let p = await withTimeout(0)`echo 'test'`
   assert.equal(p.stdout.trim(), 'test')
-}
+})
 
-if (test('echo works')) {
+test('echo works', async () => {
   echo(chalk.red('foo'), chalk.green('bar'), chalk.bold('baz'))
   echo`${chalk.red('foo')} ${chalk.green('bar')} ${chalk.bold('baz')}`
   echo(await $`echo ${chalk.red('foo')}`, await $`echo ${chalk.green('bar')}`, await $`echo ${chalk.bold('baz')}`)
-}
+})
 
-if (test('spinner works')) {
+test('spinner works', async () => {
   let s = startSpinner('waiting')
 
   await sleep(1000)
   s()
-}
+})
test/full.test.mjs
@@ -12,9 +12,6 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {printTestDigest} from './test-utils.mjs'
-await import('./zx.test.mjs')
-await import('./index.test.mjs')
-await import('./experimental.test.mjs')
-
-printTestDigest()
+import './zx.test.mjs'
+import './index.test.mjs'
+import './experimental.test.mjs'
test/index.test.mjs
@@ -17,47 +17,47 @@ import chalk from 'chalk'
 import {Writable} from 'stream'
 import {Socket} from 'net'
 
-import {assert, test as t} from './test-utils.mjs'
+import {assert, testFactory} from './test-utils.mjs'
 
-const test = t.bind(null, 'index')
+const test = testFactory('index', import.meta)
 
-if (test('Only stdout is used during command substitution')) {
+test('Only stdout is used during command substitution', async () => {
   let hello = await $`echo Error >&2; echo Hello`
   let len = +(await $`echo ${hello} | wc -c`)
   assert(len === 6)
-}
+})
 
-if (test('Env vars works')) {
+test('Env vars works', async () => {
   process.env.FOO = 'foo'
   let foo = await $`echo $FOO`
   assert(foo.stdout === 'foo\n')
-}
+})
 
-if (test('Env vars is safe to pass')) {
+test('Env vars is safe to pass', async () => {
   process.env.FOO = 'hi; exit 1'
   await $`echo $FOO`
-}
+})
 
-if (test('Arguments are quoted')) {
+test('Arguments are quoted', async () => {
   let bar = 'bar"";baz!$#^$\'&*~*%)({}||\\/'
   assert((await $`echo ${bar}`).stdout.trim() === bar)
-}
+})
 
-if (test('Undefined and empty string correctly quoted')) {
+test('Undefined and empty string correctly quoted', async () => {
   $`echo ${undefined}`
   $`echo ${''}`
-}
+})
 
-if (test('Can create a dir with a space in the name')) {
+test('Can create a dir with a space in the name', async () => {
   let name = 'foo bar'
   try {
     await $`mkdir /tmp/${name}`
   } finally {
     await fs.rmdir('/tmp/' + name)
   }
-}
+})
 
-if (test('Pipefail is on')) {
+test('Pipefail is on', async () => {
   let p
   try {
     p = await $`cat /dev/not_found | sort`
@@ -66,37 +66,37 @@ if (test('Pipefail is on')) {
     p = e
   }
   assert(p.exitCode !== 0)
-}
+})
 
-if (test('The __filename & __dirname are defined')) {
+test('The __filename & __dirname are defined', async () => {
   console.log(__filename, __dirname)
-}
+})
 
-if (test('The toString() is called on arguments')) {
+test('The toString() is called on arguments', async () => {
   let foo = 0
   let p = await $`echo ${foo}`
   assert(p.stdout === '0\n')
-}
+})
 
-if (test('Can use array as an argument')) {
+test('Can use array as an argument', async () => {
   try {
     let files = ['./zx.mjs', './test/index.test.mjs']
     await $`tar czf archive ${files}`
   } finally {
     await $`rm archive`
   }
-}
+})
 
-if (test('Quiet mode is working')) {
+test('Quiet mode is working', async () => {
   let stdout = ''
   let log = console.log
   console.log = (...args) => {stdout += args.join(' ')}
   await quiet($`echo 'test'`)
   console.log = log
   assert(!stdout.includes('echo'))
-}
+})
 
-if (test('Pipes are working')) {
+test('Pipes are working', async () => {
   let {stdout} = await $`echo "hello"`
     .pipe($`awk '{print $1" world"}'`)
     .pipe($`tr '[a-z]' '[A-Z]'`)
@@ -114,9 +114,9 @@ if (test('Pipes are working')) {
   } finally {
     await fs.rm('/tmp/output.txt')
   }
-}
+})
 
-if (test('question')) {
+test('question', async () => {
   let p = question('foo or bar? ', {choices: ['foo', 'bar']})
 
   setImmediate(() => {
@@ -126,9 +126,9 @@ if (test('question')) {
   })
 
   assert.equal(await p, 'foo')
-}
+})
 
-if (test('ProcessPromise')) {
+test('ProcessPromise', async () => {
   let contents = ''
   let stream = new Writable({
     write: function(chunk, encoding, next) {
@@ -149,9 +149,9 @@ if (test('ProcessPromise')) {
     err = p
   }
   assert.equal(err.message, 'The pipe() method does not take strings. Forgot $?')
-}
+})
 
-if (test('ProcessOutput thrown as error')) {
+test('ProcessOutput thrown as error', async () => {
   let err
   try {
     await $`wtf`
@@ -161,9 +161,9 @@ if (test('ProcessOutput thrown as error')) {
   assert(err.exitCode > 0)
   assert(err.stderr.includes('/bin/bash: wtf: command not found\n'))
   assert(err[inspect.custom]().includes('Command not found'))
-}
+})
 
-if (test('The pipe() throws if already resolved')) {
+test('The pipe() throws if already resolved', async () => {
   let out, p = $`echo "Hello"`
   await p
   try {
@@ -174,19 +174,19 @@ if (test('The pipe() throws if already resolved')) {
   if (out) {
     assert.fail('Expected failure!')
   }
-}
+})
 
-if (test('ProcessOutput::exitCode do not throw')) {
+test('ProcessOutput::exitCode do not throw', async () => {
   assert(await $`grep qwerty README.md`.exitCode !== 0)
   assert(await $`[[ -f ${__filename} ]]`.exitCode === 0)
-}
+})
 
-if (test('The nothrow() do not throw')) {
+test('The nothrow() do not throw', async () => {
   let {exitCode} = await nothrow($`exit 42`)
   assert(exitCode === 42)
-}
+})
 
-if (test('globby available')) {
+test('globby available', async () => {
   assert(globby === glob)
   assert(typeof globby === 'function')
   assert(typeof globby.globbySync === 'function')
@@ -202,16 +202,16 @@ if (test('globby available')) {
     'test/fixtures/no-extension',
     'test/fixtures/no-extension.mjs'
   ])
-}
+})
 
-if (test('fetch')) {
+test('fetch', async () => {
   assert(
     await fetch('https://example.com'),
     await fetch('https://example.com', {method: 'GET'})
   )
-}
+})
 
-if (test('Executes a script from $PATH')) {
+test('Executes a script from $PATH', async () => {
   const isWindows = process.platform === 'win32'
   const oldPath = process.env.PATH
 
@@ -233,9 +233,9 @@ if (test('Executes a script from $PATH')) {
     process.env.PATH = oldPath
     fs.rmSync('/tmp/script-from-path')
   }
-}
+})
 
-if (test('The cd() works with relative paths')) {
+test('The cd() works with relative paths', async () => {
   let cwd = process.cwd()
   try {
     fs.mkdirpSync('/tmp/zx-cd-test/one/two')
@@ -254,17 +254,17 @@ if (test('The cd() works with relative paths')) {
     fs.rmSync('/tmp/zx-cd-test', {recursive: true})
     cd(cwd)
   }
-}
+})
 
-if (test('The kill() method works')) {
+test('The kill() method works', async () => {
   let p = nothrow($`sleep 9999`)
   setTimeout(() => {
     p.kill()
   }, 100)
   await p
-}
+})
 
-if (test('The signal is passed with kill() method')) {
+test('The signal is passed with kill() method', async () => {
   let p = $`while true; do :; done`
   setTimeout(() => p.kill('SIGKILL'), 100)
   let signal
@@ -274,19 +274,19 @@ if (test('The signal is passed with kill() method')) {
     signal = p.signal
   }
   assert.equal(signal, 'SIGKILL')
-}
+})
 
-if (test('YAML works')) {
+test('YAML works', async () => {
   assert.deepEqual(YAML.parse(YAML.stringify({foo: 'bar'})), {foo: 'bar'})
   console.log(chalk.greenBright('YAML works'))
-}
+})
 
-if (test('which available')) {
+test('which available', async () => {
   assert.equal(which.sync('npm'), await which('npm'))
-}
+})
 
-if (test('require() is working in ESM')) {
+test('require() is working in ESM', async () => {
   let data = require('../package.json')
   assert.equal(data.name, 'zx')
   assert.equal(data, require('zx/package.json'))
-}
+})
test/test-utils.mjs
@@ -13,24 +13,88 @@
 // limitations under the License.
 
 import chalk from 'chalk'
+import {fileURLToPath} from 'node:url'
+import {relative} from 'node:path'
+import {sleep} from '../src/index.mjs'
 
 export {strict as assert} from 'assert'
 
-let всегоТестов = 0
+let queued = 0
+let passed = 0
+let failed = 0
+let total = 0
+let skipped = 0
+let focused = 0
 
-export function test(group, name) {
-  let фильтр = process.argv[3] || '.'
-  if (RegExp(фильтр).test(name) || RegExp(фильтр).test(group)) {
-    console.log('\n' + chalk.bgGreenBright.black(`${chalk.inverse(' ' + group + ' ')} ${name} `))
-    всегоТестов++
-    return true
+const singleThread = (fn) => {
+  let p = Promise.resolve()
+  return async function (...args) {
+    return (p = p.catch(_ => _).then(() => fn.call(this, ...args)))
   }
-  return false
 }
 
+const run = singleThread((cb) => cb())
+
+const warmup = sleep(100)
+
+const log = (name, group, err, file = '') => {
+  if (err) {
+    console.log(err)
+    console.log(file)
+  }
+  console.log('\n' + chalk[ err ? 'bgRedBright' : 'bgGreenBright' ].black(`${chalk.inverse(' ' + group + ' ')} ${name} `))
+}
+
+export const test = async function (name, cb, ms, focus, skip) {
+  const filter = RegExp(process.argv[3] || '.')
+  const {group, meta} = this
+  const file = meta ? relative(process.cwd(), fileURLToPath(meta.url)) : ''
+
+  if (filter.test(name) || filter.test(group) || filter.test(file)) {
+    focused += +!!focus
+    queued++
+
+    await warmup
+    try {
+      if (!focused === !focus && !skip) {
+        await run(cb)
+        passed++
+        log(name, group)
+      } else {
+        skipped ++
+      }
+    } catch (e) {
+      log(name, group, e, file)
+      failed++
+    } finally {
+      total++
+
+      if (total === queued) {
+        printTestDigest()
+      }
+    }
+  }
+}
+
+export const only = async function (name, cb, ms) { return test.call(this, name, cb, ms, true, false) }
+
+export const skip = async function (name, cb, ms) { return test.call(this, name, cb, ms, false, true) }
+
+export const testFactory = (group, meta) => Object.assign(
+  test.bind({group, meta}), {
+    test,
+    skip,
+    only,
+    group,
+    meta
+  })
+
 export const printTestDigest = () => {
   console.log('\n' +
     chalk.black.bgYellowBright(` zx version is ${require('../package.json').version} `) + '\n' +
-    chalk.greenBright(` 🍺 ${всегоТестов} tests passed `)
+    chalk.greenBright(` 🍺 tests passed: ${passed} `) +
+    (skipped ? chalk.yellowBright (`\n 🚧 skipped: ${skipped} `) : '') +
+    (failed ? chalk.redBright (`\n ❌  failed: ${failed} `) : '')
   )
+  failed && process.exit(1)
 }
test/zx.test.mjs
@@ -12,16 +12,16 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {assert, test as t} from './test-utils.mjs'
+import {assert, testFactory} from './test-utils.mjs'
 
-const test = t.bind(null, 'zx')
+const test = testFactory('zx', import.meta)
 
-if (test('supports `-v` flag / prints version')) {
+test('supports `-v` flag / prints version', async () => {
   let v = (await $`node zx.mjs -v`).toString().trim()
   assert.equal(v, require('../package.json').version)
-}
+})
 
-if (test('prints help')) {
+test('prints help', async () => {
   let help
   try {
     await $`node zx.mjs`
@@ -29,30 +29,30 @@ if (test('prints help')) {
     help = err.toString().trim()
   }
   assert(help.includes('print current zx version'))
-}
+})
 
-if (test('supports `--experimental` flag')) {
+test('supports `--experimental` flag', async () => {
   await $`echo 'echo("test")' | node zx.mjs --experimental`
-}
+})
 
-if (test('supports `--quiet` flag / Quiet mode is working')) {
+test('supports `--quiet` flag / Quiet mode is working', async () => {
   let p = await $`node zx.mjs --quiet docs/markdown.md`
   assert(!p.stdout.includes('whoami'))
-}
+})
 
-if (test('supports `--shell` flag ')) {
+test('supports `--shell` flag ', async () => {
   let shell = $.shell
   let p = await $`node zx.mjs --shell=${shell} <<< '$\`echo \${$.shell}\`'`
   assert(p.stdout.includes(shell))
-}
+})
 
-if (test('supports `--prefix` flag ')) {
+test('supports `--prefix` flag ', async () => {
   let prefix = 'set -e;'
   let p = await $`node zx.mjs --prefix=${prefix} <<< '$\`echo \${$.prefix}\`'`
   assert(p.stdout.includes(prefix))
-}
+})
 
-if (test('Eval script from https ref')) {
+test('Eval script from https ref', async () => {
   let script = path.resolve('test/fixtures/echo.http')
   let server = quiet($`while true; do cat ${script} | nc -l 8080; done`)
   let p = await quiet($`node zx.mjs http://127.0.0.1:8080/echo.mjs`)
@@ -67,17 +67,17 @@ if (test('Eval script from https ref')) {
     err = e
   }
   assert(err.stderr.includes('ECONNREFUSED'))
-}
+})
 
-if (test('Scripts with no extension')) {
+test('Scripts with no extension', async () => {
   await $`node zx.mjs test/fixtures/no-extension`
   assert.match((await fs.readFile('test/fixtures/no-extension.mjs')).toString(), /Test file to verify no-extension didn't overwrite similarly name .mjs file./)
-}
+})
 
-if (test('The require() is working from stdin')) {
+test('The require() is working from stdin', async () => {
   await $`node zx.mjs <<< 'require("./package.json").name'`
-}
+})
 
-if (test('Markdown scripts are working')) {
+test('Markdown scripts are working', async () => {
   await $`node zx.mjs docs/markdown.md`
-}
+})
package.json
@@ -45,6 +45,9 @@
     "which": "^2.0.2",
     "yaml": "^1.10.2"
   },
+  "devDependencies": {
+    "c8": "^7.11.0"
+  },
   "publishConfig": {
     "registry": "https://wombat-dressing-room.appspot.com"
   },