Commit 20960b7

Anton Medvedev <anton@medv.io>
2022-05-29 23:50:42
Migrate to ava
1 parent c3fbe9a
src/core.ts
@@ -205,12 +205,15 @@ export class ProcessPromise extends Promise<ProcessOutput> {
       )
 
       child.on('close', (code, signal) => {
-        let message = `${stderr || '\n'}    at ${__from}`
-        message += `\n    exit code: ${code}${
-          exitCodeInfo(code) ? ' (' + exitCodeInfo(code) + ')' : ''
-        }`
-        if (signal !== null) {
-          message += `\n    signal: ${signal}`
+        let message = `exit code: ${code}`
+        if (code != 0 || signal != null) {
+          message = `${stderr || '\n'}    at ${__from}`
+          message += `\n    exit code: ${code}${
+            exitCodeInfo(code) ? ' (' + exitCodeInfo(code) + ')' : ''
+          }`
+          if (signal != null) {
+            message += `\n    signal: ${signal}`
+          }
         }
         let output = new ProcessOutput({
           code,
test/fixtures/cd-parallel-contexts.mjs
@@ -0,0 +1,34 @@
+import { strict as assert } from 'assert'
+import { ProcessPromise } from '../../build/index.js'
+import { runInCtx, getCtx } from '../../build/experimental.js'
+
+let cwd = process.cwd()
+let resolve, reject
+let promise = new ProcessPromise((...args) => ([resolve, reject] = args))
+
+try {
+  fs.mkdirpSync('/tmp/zx-cd-parallel')
+  runInCtx({ ...getCtx() }, async () => {
+    assert.equal($.cwd, cwd)
+    await sleep(10)
+    cd('/tmp/zx-cd-parallel')
+    assert.ok(getCtx().cwd.endsWith('/zx-cd-parallel'))
+    assert.ok($.cwd.endsWith('/zx-cd-parallel'))
+  })
+
+  runInCtx({ ...getCtx() }, async () => {
+    assert.equal($.cwd, cwd)
+    assert.equal(getCtx().cwd, cwd)
+    await sleep(20)
+    assert.equal(getCtx().cwd, cwd)
+    assert.ok($.cwd.endsWith('/zx-cd-parallel'))
+    resolve()
+  })
+
+  await promise
+} catch (e) {
+  assert(!e, e)
+} finally {
+  fs.rmSync('/tmp/zx-cd-parallel', { recursive: true })
+  cd(cwd)
+}
test/fixtures/cd-relative-paths.mjs
@@ -0,0 +1,33 @@
+import { strict as assert } from 'assert'
+
+let cwd = process.cwd()
+assert.equal($.cwd, cwd)
+try {
+  fs.mkdirpSync('/tmp/zx-cd-test/one/two')
+  cd('/tmp/zx-cd-test/one/two')
+  let p1 = $`pwd`
+  assert.ok($.cwd.endsWith('/two'))
+  assert.ok(process.cwd().endsWith('/two'))
+
+  cd('..')
+  let p2 = $`pwd`
+  assert.ok($.cwd.endsWith('/one'))
+  assert.ok(process.cwd().endsWith('/one'))
+
+  cd('..')
+  let p3 = $`pwd`
+  assert.ok(process.cwd().endsWith('/zx-cd-test'))
+  assert.ok($.cwd.endsWith('/tmp/zx-cd-test'))
+
+  let results = (await Promise.all([p1, p2, p3])).map((p) =>
+    path.basename(p.stdout.trim())
+  )
+
+  assert.deepEqual(results, ['two', 'one', 'zx-cd-test'])
+} catch (e) {
+  assert(!e, e)
+} finally {
+  fs.rmSync('/tmp/zx-cd-test', { recursive: true })
+  cd(cwd)
+  assert.equal($.cwd, cwd)
+}
test/fixtures/filename-dirname.mjs
@@ -0,0 +1,4 @@
+import { strict } from 'assert'
+
+strict.equal(path.basename(__filename), 'filename-dirname.mjs')
+strict.equal(path.basename(__dirname), 'fixtures')
test/fixtures/kill-signal.mjs
@@ -0,0 +1,13 @@
+import { strict } from 'assert'
+
+let p = $`while true; do :; done`
+setTimeout(() => p.kill('SIGKILL'), 100)
+let signal
+
+try {
+  await p
+} catch (p) {
+  signal = p.signal
+}
+
+strict.equal(signal, 'SIGKILL')
test/fixtures/kill.mjs
@@ -0,0 +1,5 @@
+let p = nothrow($`sleep 9999`)
+setTimeout(() => {
+  p.kill()
+}, 100)
+await p
test/fixtures/require.mjs
@@ -0,0 +1,4 @@
+import { strict as assert } from 'assert'
+let data = require('../../package.json')
+assert.equal(data.name, 'zx')
+assert.equal(data, require('zx/package.json'))
test/cli.test.js
@@ -12,52 +12,55 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import { assert, testFactory } from './test-utils.js'
+import test from 'ava'
+import '../build/globals.js'
 
-const test = testFactory('cli', import.meta)
+$.verbose = false
 
-test('supports `-v` flag / prints version', async () => {
-  let v = (await $`node build/cli.js -v`).toString().trim()
-  assert.equal(v, require('../package.json').version)
+test('supports `-v` flag / prints version', async (t) => {
+  t.regex((await $`node build/cli.js -v`).toString(), /\d+.\d+.\d+/)
 })
 
-test('prints help', async () => {
+test('prints help', async (t) => {
   let help
   try {
     await $`node build/cli.js`
   } catch (err) {
     help = err.toString().trim()
   }
-  assert(help.includes('print current zx version'))
+  t.true(help.includes('print current zx version'))
 })
 
-test('supports `--experimental` flag', async () => {
+test('supports `--experimental` flag', async (t) => {
   await $`echo 'echo("test")' | node build/cli.js --experimental`
+  t.pass()
 })
 
-test('supports `--quiet` flag / Quiet mode is working', async () => {
+test('supports `--quiet` flag / Quiet mode is working', async (t) => {
   let p = await $`node build/cli.js --quiet docs/markdown.md`
-  assert(!p.stdout.includes('whoami'))
+  t.true(!p.stdout.includes('whoami'))
 })
 
-test('supports `--shell` flag ', async () => {
+test('supports `--shell` flag ', async (t) => {
   let shell = $.shell
-  let p = await $`node build/cli.js --shell=${shell} <<< '$\`echo \${$.shell}\`'`
-  assert(p.stdout.includes(shell))
+  let p =
+    await $`node build/cli.js --shell=${shell} <<< '$\`echo \${$.shell}\`'`
+  t.true(p.stdout.includes(shell))
 })
 
-test('supports `--prefix` flag ', async () => {
+test('supports `--prefix` flag ', async (t) => {
   let prefix = 'set -e;'
-  let p = await $`node build/cli.js --prefix=${prefix} <<< '$\`echo \${$.prefix}\`'`
-  assert(p.stdout.includes(prefix))
+  let p =
+    await $`node build/cli.js --prefix=${prefix} <<< '$\`echo \${$.prefix}\`'`
+  t.true(p.stdout.includes(prefix))
 })
 
-test('scripts from https', async () => {
+test('scripts from https', async (t) => {
   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 build/cli.js http://127.0.0.1:8080/echo.mjs`)
 
-  assert(p.stdout.includes('test'))
+  t.true(p.stdout.includes('test'))
   server.kill()
 
   let err
@@ -66,21 +69,34 @@ test('scripts from https', async () => {
   } catch (e) {
     err = e
   }
-  assert(err.stderr.includes('ECONNREFUSED'))
+  t.true(err.stderr.includes('ECONNREFUSED'))
 })
 
-test('scripts with no extension', async () => {
+test('scripts with no extension', async (t) => {
   await $`node build/cli.js 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./
+  t.true(
+    /Test file to verify no-extension didn't overwrite similarly name .mjs file./.test(
+      (await fs.readFile('test/fixtures/no-extension.mjs')).toString()
+    )
   )
 })
 
-test('The require() is working from stdin', async () => {
+test('require() is working from stdin', async (t) => {
   await $`node build/cli.js <<< 'require("./package.json").name'`
+  t.pass()
 })
 
-test('Markdown scripts are working', async () => {
+test('require() is working in ESM', async (t) => {
+  await $`node build/cli.js test/fixtures/require.mjs`
+  t.pass()
+})
+
+test('__filename & __dirname are defined', async (t) => {
+  await $`node build/cli.js test/fixtures/filename-dirname.mjs`
+  t.pass()
+})
+
+test('markdown scripts are working', async (t) => {
   await $`node build/cli.js docs/markdown.md`
+  t.pass()
 })
test/experimental.test.js
@@ -12,18 +12,19 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import test from 'ava'
+import '../build/globals.js'
+
+$.verbose = false
+
 import {
   echo,
   retry,
   startSpinner,
   withTimeout,
 } from '../build/experimental.js'
-import { assert, testFactory } from './test-utils.js'
-import chalk from 'chalk'
 
-const test = testFactory('experimental', import.meta)
-
-test('Retry works', async () => {
+test('Retry works', async (t) => {
   let exitCode = 0
   let now = Date.now()
   try {
@@ -31,11 +32,11 @@ test('Retry works', async () => {
   } catch (p) {
     exitCode = p.exitCode
   }
-  assert.equal(exitCode, 123)
-  assert(Date.now() >= now + 50 * (5 - 1))
+  t.is(exitCode, 123)
+  t.true(Date.now() >= now + 50 * (5 - 1))
 })
 
-test('withTimeout works', async () => {
+test('withTimeout works', async (t) => {
   let exitCode = 0
   let signal
   try {
@@ -44,26 +45,27 @@ test('withTimeout works', async () => {
     exitCode = p.exitCode
     signal = p.signal
   }
-  assert.equal(exitCode, null)
-  assert.equal(signal, 'SIGKILL')
+  t.is(exitCode, null)
+  t.is(signal, 'SIGKILL')
 
   let p = await withTimeout(0)`echo 'test'`
-  assert.equal(p.stdout.trim(), 'test')
+  t.is(p.stdout.trim(), 'test')
 })
 
-test('echo works', async () => {
-  echo(chalk.red('foo'), chalk.green('bar'), chalk.bold('baz'))
-  echo`${chalk.red('foo')} ${chalk.green('bar')} ${chalk.bold('baz')}`
+test('echo works', async (t) => {
+  echo(chalk.cyan('foo'), chalk.green('bar'), chalk.bold('baz'))
+  echo`${chalk.cyan('foo')} ${chalk.green('bar')} ${chalk.bold('baz')}`
   echo(
-    await $`echo ${chalk.red('foo')}`,
+    await $`echo ${chalk.cyan('foo')}`,
     await $`echo ${chalk.green('bar')}`,
     await $`echo ${chalk.bold('baz')}`
   )
+  t.pass()
 })
 
-test('spinner works', async () => {
+test('spinner works', async (t) => {
   let s = startSpinner('waiting')
-
   await sleep(1000)
   s()
+  t.pass()
 })
test/full.test.js
@@ -1,17 +0,0 @@
-// Copyright 2022 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     https://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import './cli.test.js'
-import './index.test.js'
-import './experimental.test.js'
test/index.test.js
@@ -12,54 +12,56 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import test from 'ava'
 import { inspect } from 'node:util'
 import chalk from 'chalk'
 import { Writable } from 'node:stream'
 import { Socket } from 'node:net'
-
-import { assert, testFactory } from './test-utils.js'
+import '../build/globals.js'
 import { ProcessPromise } from '../build/index.js'
-import { getCtx, runInCtx } from '../build/context.js'
 
-const test = testFactory('index', import.meta)
+$.verbose = false
 
-test('Only stdout is used during command substitution', async () => {
+test('Only stdout is used during command substitution', async (t) => {
   let hello = await $`echo Error >&2; echo Hello`
   let len = +(await $`echo ${hello} | wc -c`)
-  assert(len === 6)
+  t.is(len, 6)
 })
 
-test('Env vars works', async () => {
-  process.env.FOO = 'foo'
-  let foo = await $`echo $FOO`
-  assert(foo.stdout === 'foo\n')
+test('Env vars works', async (t) => {
+  process.env.ZX_TEST_FOO = 'foo'
+  let foo = await $`echo $ZX_TEST_FOO`
+  t.is(foo.stdout, 'foo\n')
 })
 
-test('Env vars is safe to pass', async () => {
-  process.env.FOO = 'hi; exit 1'
-  await $`echo $FOO`
+test('Env vars is safe to pass', async (t) => {
+  process.env.ZX_TEST_BAR = 'hi; exit 1'
+  await $`echo $ZX_TEST_BAR`
+  t.pass()
 })
 
-test('Arguments are quoted', async () => {
+test('Arguments are quoted', async (t) => {
   let bar = 'bar"";baz!$#^$\'&*~*%)({}||\\/'
-  assert((await $`echo ${bar}`).stdout.trim() === bar)
+  t.is((await $`echo ${bar}`).stdout.trim(), bar)
 })
 
-test('Undefined and empty string correctly quoted', async () => {
-  $`echo ${undefined}`
-  $`echo ${''}`
+test('Undefined and empty string correctly quoted', async (t) => {
+  t.verbose = true
+  t.is((await $`echo -n ${undefined}`).toString(), 'undefined')
+  t.is((await $`echo -n ${''}`).toString(), '')
 })
 
-test('Can create a dir with a space in the name', async () => {
+test('Can create a dir with a space in the name', async (t) => {
   let name = 'foo bar'
   try {
     await $`mkdir /tmp/${name}`
   } finally {
     await fs.rmdir('/tmp/' + name)
   }
+  t.pass()
 })
 
-test('Pipefail is on', async () => {
+test('Pipefail is on', async (t) => {
   let p
   try {
     p = await $`cat /dev/not_found | sort`
@@ -67,29 +69,21 @@ test('Pipefail is on', async () => {
     console.log('Caught an exception -> ok')
     p = e
   }
-  assert(p.exitCode !== 0)
-})
-
-test('The __filename & __dirname are defined', async () => {
-  console.log(__filename, __dirname)
+  t.not(p.exitCode, 0)
 })
 
-test('The toString() is called on arguments', async () => {
+test('The toString() is called on arguments', async (t) => {
   let foo = 0
   let p = await $`echo ${foo}`
-  assert(p.stdout === '0\n')
+  t.is(p.stdout, '0\n')
 })
 
-test('Can use array as an argument', async () => {
-  try {
-    let files = ['./cli.ts', './test/index.test.js']
-    await $`tar czf archive ${files}`
-  } finally {
-    await $`rm archive`
-  }
+test('Can use array as an argument', async (t) => {
+  let args = ['-n', 'foo']
+  t.is((await $`echo ${args}`).toString(), 'foo')
 })
 
-test('Quiet mode is working', async () => {
+test.serial('Quiet mode is working', async (t) => {
   let stdout = ''
   let log = console.log
   console.log = (...args) => {
@@ -97,28 +91,28 @@ test('Quiet mode is working', async () => {
   }
   await quiet($`echo 'test'`)
   console.log = log
-  assert(!stdout.includes('echo'))
+  t.is(stdout, '')
 })
 
-test('Pipes are working', async () => {
+test('Pipes are working', async (t) => {
   let { stdout } = await $`echo "hello"`
     .pipe($`awk '{print $1" world"}'`)
     .pipe($`tr '[a-z]' '[A-Z]'`)
-  assert(stdout === 'HELLO WORLD\n')
+  t.is(stdout, 'HELLO WORLD\n')
 
   try {
     await $`echo foo`.pipe(fs.createWriteStream('/tmp/output.txt'))
-    assert((await fs.readFile('/tmp/output.txt')).toString() === 'foo\n')
+    t.is((await fs.readFile('/tmp/output.txt')).toString(), 'foo\n')
 
     let r = $`cat`
     fs.createReadStream('/tmp/output.txt').pipe(r.stdin)
-    assert((await r).stdout === 'foo\n')
+    t.is((await r).stdout, 'foo\n')
   } finally {
     await fs.rm('/tmp/output.txt')
   }
 })
 
-test('question', async () => {
+test('question', async (t) => {
   let p = question('foo or bar? ', { choices: ['foo', 'bar'] })
 
   setImmediate(() => {
@@ -127,10 +121,10 @@ test('question', async () => {
     process.stdin.emit('data', '\n')
   })
 
-  assert.equal(await p, 'foo')
+  t.is(await p, 'foo')
 })
 
-test('ProcessPromise', async () => {
+test('ProcessPromise', async (t) => {
   let contents = ''
   let stream = new Writable({
     write: function (chunk, encoding, next) {
@@ -140,9 +134,9 @@ test('ProcessPromise', async () => {
   })
   let p = $`echo 'test'`.pipe(stream)
   await p
-  assert(p._piped)
-  assert.equal(contents, 'test\n')
-  assert(p.stderr instanceof Socket)
+  t.true(p._piped)
+  t.is(contents, 'test\n')
+  t.true(p.stderr instanceof Socket)
 
   let err
   try {
@@ -150,96 +144,86 @@ test('ProcessPromise', async () => {
   } catch (p) {
     err = p
   }
-  assert.equal(
-    err.message,
-    'The pipe() method does not take strings. Forgot $?'
-  )
+  t.is(err.message, 'The pipe() method does not take strings. Forgot $?')
 })
 
-test('ProcessPromise: inherits native Promise', async () => {
+test('ProcessPromise: inherits native Promise', async (t) => {
   const p1 = $`echo 1`
   const p2 = p1.then((v) => v)
   const p3 = p2.then((v) => v)
   const p4 = p3.catch((v) => v)
   const p5 = p1.finally((v) => v)
 
-  assert.ok(p1 instanceof Promise)
-  assert.ok(p1 instanceof ProcessPromise)
-  assert.ok(p2 instanceof ProcessPromise)
-  assert.ok(p3 instanceof ProcessPromise)
-  assert.ok(p4 instanceof ProcessPromise)
-  assert.ok(p5 instanceof ProcessPromise)
-  assert.ok(p1 !== p2)
-  assert.ok(p2 !== p3)
-  assert.ok(p3 !== p4)
-  assert.ok(p5 !== p1)
+  t.true(p1 instanceof Promise)
+  t.true(p1 instanceof ProcessPromise)
+  t.true(p2 instanceof ProcessPromise)
+  t.true(p3 instanceof ProcessPromise)
+  t.true(p4 instanceof ProcessPromise)
+  t.true(p5 instanceof ProcessPromise)
+  t.true(p1 !== p2)
+  t.true(p2 !== p3)
+  t.true(p3 !== p4)
+  t.true(p5 !== p1)
 })
 
-test('ProcessOutput thrown as error', async () => {
+test('ProcessOutput thrown as error', async (t) => {
   let err
   try {
     await $`wtf`
   } catch (p) {
     err = p
   }
-  assert(err.exitCode > 0)
-  assert(err.stderr.includes('/bin/bash: wtf: command not found\n'))
-  assert(err[inspect.custom]().includes('Command not found'))
+  t.true(err.exitCode > 0)
+  t.true(err.stderr.includes('/bin/bash: wtf: command not found\n'))
+  t.true(err[inspect.custom]().includes('Command not found'))
 })
 
-test('The pipe() throws if already resolved', async () => {
+test('The pipe() throws if already resolved', async (t) => {
   let out,
     p = $`echo "Hello"`
   await p
   try {
     out = await p.pipe($`less`)
   } catch (err) {
-    assert.equal(
+    t.is(
       err.message,
       `The pipe() method shouldn't be called after promise is already resolved!`
     )
   }
   if (out) {
-    assert.fail('Expected failure!')
+    t.fail('Expected failure!')
   }
 })
 
-test('ProcessOutput::exitCode do not throw', async () => {
-  assert((await $`grep qwerty README.md`.exitCode) !== 0)
-  assert((await $`[[ -f ${__filename} ]]`.exitCode) === 0)
+test('await $`cmd`.exitCode does not throw', async (t) => {
+  t.not(await $`grep qwerty README.md`.exitCode, 0)
+  t.is(await $`[[ -f README.md ]]`.exitCode, 0)
 })
 
-test('The nothrow() do not throw', async () => {
+test('The nothrow() do not throw', async (t) => {
   let { exitCode } = await nothrow($`exit 42`)
-  assert(exitCode === 42)
+  t.is(exitCode, 42)
 })
 
-test('globby available', async () => {
-  assert(globby === glob)
-  assert(typeof globby === 'function')
-  assert(typeof globby.globbySync === 'function')
-  assert(typeof globby.globbyStream === 'function')
-  assert(typeof globby.generateGlobTasks === 'function')
-  assert(typeof globby.isDynamicPattern === 'function')
-  assert(typeof globby.isGitIgnored === 'function')
-  assert(typeof globby.isGitIgnoredSync === 'function')
+test('globby available', async (t) => {
+  t.is(globby, glob)
+  t.is(typeof globby, 'function')
+  t.is(typeof globby.globbySync, 'function')
+  t.is(typeof globby.globbyStream, 'function')
+  t.is(typeof globby.generateGlobTasks, 'function')
+  t.is(typeof globby.isDynamicPattern, 'function')
+  t.is(typeof globby.isGitIgnored, 'function')
+  t.is(typeof globby.isGitIgnoredSync, 'function')
   console.log(chalk.greenBright('globby available'))
 
-  assert(await globby('test/fixtures/*'), [
-    'test/fixtures/interactive.mjs',
-    'test/fixtures/no-extension',
-    'test/fixtures/no-extension.mjs',
-  ])
+  t.deepEqual(await globby('*.md'), ['README.md'])
 })
 
-test('fetch', async () => {
-  assert(
-    await fetch('https://example.com'),
-    await fetch('https://example.com', { method: 'GET' })
-  )
+test('fetch', async t => {
+  t.regex(await fetch('https://medv.io').then(res => res.text()), /Anton Medvedev/)
 })
 
-test('Executes a script from $PATH', async () => {
+test('Executes a script from $PATH', async (t) => {
   const isWindows = process.platform === 'win32'
   const oldPath = process.env.PATH
 
@@ -262,106 +246,33 @@ test('Executes a script from $PATH', async () => {
     process.env.PATH = oldPath
     fs.rmSync('/tmp/script-from-path')
   }
+  t.pass()
 })
 
-test('The cd() works with relative paths', async () => {
-  let cwd = process.cwd()
-  assert.equal($.cwd, cwd)
-  try {
-    fs.mkdirpSync('/tmp/zx-cd-test/one/two')
-    cd('/tmp/zx-cd-test/one/two')
-    let p1 = $`pwd`
-    assert.ok($.cwd.endsWith('/two'))
-    assert.ok(process.cwd().endsWith('/two'))
-
-    cd('..')
-    let p2 = $`pwd`
-    assert.ok($.cwd.endsWith('/one'))
-    assert.ok(process.cwd().endsWith('/one'))
-
-    cd('..')
-    let p3 = $`pwd`
-    assert.ok(process.cwd().endsWith('/zx-cd-test'))
-    assert.ok($.cwd.endsWith('/tmp/zx-cd-test'))
-
-    let results = (await Promise.all([p1, p2, p3])).map((p) =>
-      path.basename(p.stdout.trim())
-    )
-
-    assert.deepEqual(results, ['two', 'one', 'zx-cd-test'])
-  } catch (e) {
-    assert(!e, e)
-  } finally {
-    fs.rmSync('/tmp/zx-cd-test', { recursive: true })
-    cd(cwd)
-    assert.equal($.cwd, cwd)
-  }
+test('The cd() works with relative paths', async (t) => {
+  await $`node build/cli.js test/fixtures/cd-relative-paths.mjs`
+  t.pass()
 })
 
-test('cd() does not affect parallel contexts', async () => {
-  let cwd = process.cwd()
-  let resolve, reject
-  let promise = new ProcessPromise((...args) => ([resolve, reject] = args))
-
-  try {
-    fs.mkdirpSync('/tmp/zx-cd-parallel')
-    runInCtx({ ...getCtx() }, async () => {
-      assert.equal($.cwd, cwd)
-      await sleep(10)
-      cd('/tmp/zx-cd-parallel')
-      assert.ok(getCtx().cwd.endsWith('/zx-cd-parallel'))
-      assert.ok($.cwd.endsWith('/zx-cd-parallel'))
-    })
-
-    runInCtx({ ...getCtx() }, async () => {
-      assert.equal($.cwd, cwd)
-      assert.equal(getCtx().cwd, cwd)
-      await sleep(20)
-      assert.equal(getCtx().cwd, cwd)
-      assert.ok($.cwd.endsWith('/zx-cd-parallel'))
-      resolve()
-    })
-
-    await promise
-  } catch (e) {
-    assert(!e, e)
-  } finally {
-    fs.rmSync('/tmp/zx-cd-parallel', { recursive: true })
-    cd(cwd)
-  }
+test('cd() does not affect parallel contexts', async (t) => {
+  await $`node build/cli.js test/fixtures/cd-parallel-contexts.mjs`
+  t.pass()
 })
 
-test('The kill() method works', async () => {
-  let p = nothrow($`sleep 9999`)
-  setTimeout(() => {
-    p.kill()
-  }, 100)
-  await p
-})
-
-test('The signal is passed with kill() method', async () => {
-  let p = $`while true; do :; done`
-  setTimeout(() => p.kill('SIGKILL'), 100)
-  let signal
-  try {
-    await p
-  } catch (p) {
-    signal = p.signal
-  }
-  assert.equal(signal, 'SIGKILL')
+test('The kill() method works', async (t) => {
+  await $`node build/cli.js test/fixtures/kill.mjs`
+  t.pass()
 })
 
-test('YAML works', async () => {
-  assert.deepEqual(YAML.parse(YAML.stringify({ foo: 'bar' })), { foo: 'bar' })
-  console.log(chalk.greenBright('YAML works'))
+test('The signal is passed with kill() method', async (t) => {
+  await $`node build/cli.js test/fixtures/kill-signal.mjs`
+  t.pass()
 })
 
-test('which available', async () => {
-  assert.equal(which.sync('npm'), await which('npm'))
+test('YAML works', async (t) => {
+  t.deepEqual(YAML.parse(YAML.stringify({ foo: 'bar' })), { foo: 'bar' })
 })
 
-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('which available', async (t) => {
+  t.is(which.sync('npm'), await which('npm'))
 })
test/test-utils.js
@@ -1,113 +0,0 @@
-// Copyright 2022 Google LLC
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//     https://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import chalk from 'chalk'
-import { fileURLToPath } from 'node:url'
-import { relative } from 'node:path'
-import { setTimeout as sleep } from 'node:timers/promises'
-
-export { strict as assert } from 'assert'
-
-let queued = 0
-let passed = 0
-let failed = 0
-let total = 0
-let skipped = 0
-let focused = 0
-
-const singleThread = (fn) => {
-  let p = Promise.resolve()
-  return async function (...args) {
-    return (p = p.catch((_) => _).then(() => fn.call(this, ...args)))
-  }
-}
-
-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: ${passed} `) +
-      (skipped ? chalk.yellowBright(`\n ๐Ÿšง skipped: ${skipped} `) : '') +
-      (failed ? chalk.redBright(`\n โŒ failed: ${failed} `) : '')
-  )
-  failed && process.exit(1)
-}
package.json
@@ -22,8 +22,8 @@
   "scripts": {
     "fmt": "prettier --write .",
     "build": "tsc",
-    "test": "tsc && node build/cli.js test/full.test.js",
-    "coverage": "c8 --reporter=html npm run test:unit"
+    "test": "tsc && ava",
+    "coverage": "c8 --reporter=html npm test"
   },
   "dependencies": {
     "@types/fs-extra": "^9.0.13",
@@ -31,6 +31,7 @@
     "@types/node": "^17.0",
     "@types/ps-tree": "^1.1.2",
     "@types/which": "^2.0.1",
+    "ava": "^4.2.0",
     "chalk": "^5.0.1",
     "fs-extra": "^10.1.0",
     "globby": "^13.1.1",