Commit e5d56e2
Changed files (7)
.github
workflows
.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"
},