Commit a203460

Anton Golub <mailbox@antongolub.ru>
2022-03-19 18:13:56
test: measure codecov, add missing tests (#359)
* test: measure codecov, add missing tests * test: relax some asserts * test: tweak up test utils * test: fixes * test: separate tests for experimental api * test: check withTimeout w/ 0 * test: suppress concurrency * test: use local server for http test * test: fix http hang * test: add test for econnrefused * test: replace localhost with 127.0.0.1 due to nodejs 17 requirement * test: check --prefix and --shell flags
1 parent d7ed8c7
src/index.mjs
@@ -155,12 +155,14 @@ export async function question(query, options) {
   const rl = createInterface({
     input: process.stdin,
     output: process.stdout,
+    terminal: true,
     completer,
   })
-  const question = (q) => new Promise((resolve) => rl.question(q ?? '', resolve))
-  let answer = await question(query)
-  rl.close()
-  return answer
+
+  return new Promise((resolve) => rl.question(query ?? '', (answer) => {
+    rl.close()
+    resolve(answer)
+  }))
 }
 
 export async function fetch(url, init) {
test/fixtures/echo.http
@@ -0,0 +1,6 @@
+HTTP/1.1 200 OK
+Content-Type: text/javascript; charset=UTF-8
+Content-Length: 15
+Server: netcat!
+
+$`echo 'test'`
tests/interactive.mjs → test/fixtures/interactive.mjs
File renamed without changes
tests/no-extension → test/fixtures/no-extension
File renamed without changes
tests/no-extension.mjs → test/fixtures/no-extension.mjs
File renamed without changes
test/experimental.test.mjs
@@ -0,0 +1,60 @@
+// 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 {echo, retry, startSpinner, withTimeout} from '../src/experimental.mjs'
+import {assert, test as t} from './test-utils.mjs'
+import chalk from 'chalk'
+
+const test = t.bind(null, 'experimental')
+
+if (test('Retry works')) {
+  let exitCode = 0
+  let now = Date.now()
+  try {
+    await retry(5, 50)`exit 123`
+  } catch (p) {
+    exitCode = p.exitCode
+  }
+  assert.equal(exitCode, 123)
+  assert(Date.now() >= now + 50 * (5 - 1))
+}
+
+if (test('withTimeout works')) {
+  let exitCode = 0
+  let signal
+  try {
+    await withTimeout(100, 'SIGKILL')`sleep 9999`
+  } catch (p) {
+    exitCode = p.exitCode
+    signal = p.signal
+  }
+  assert.equal(exitCode, null)
+  assert.equal(signal, 'SIGKILL')
+
+  let p = await withTimeout(0)`echo 'test'`
+  assert.equal(p.stdout.trim(), 'test')
+}
+
+if (test('echo works')) {
+  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')) {
+  let s = startSpinner('waiting')
+
+  await sleep(1000)
+  s()
+}
test/full.test.mjs
@@ -0,0 +1,20 @@
+// 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 {printTestDigest} from './test-utils.mjs'
+await import('./zx.test.mjs')
+await import('./index.test.mjs')
+await import('./experimental.test.mjs')
+
+printTestDigest()
test.mjs → test/index.test.mjs
@@ -12,20 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {strict as assert} from 'assert'
-import {retry, echo, startSpinner, withTimeout } from './src/experimental.mjs'
+import {inspect} from 'util'
+import chalk from 'chalk'
+import {Writable} from 'stream'
+import {Socket} from 'net'
 
-let всегоТестов = 0
+import {assert, test as t} from './test-utils.mjs'
 
-function test(name) {
-  let фильтр = process.argv[3] || '.'
-  if (RegExp(фильтр).test(name)) {
-    console.log('\n' + chalk.bgGreenBright.black(` ${name} `))
-    всегоТестов++
-    return true
-  }
-  return false
-}
+const test = t.bind(null, 'index')
 
 if (test('Only stdout is used during command substitution')) {
   let hello = await $`echo Error >&2; echo Hello`
@@ -86,29 +80,20 @@ if (test('The toString() is called on arguments')) {
 
 if (test('Can use array as an argument')) {
   try {
-    let files = ['./zx.mjs', './test.mjs']
+    let files = ['./zx.mjs', './test/index.test.mjs']
     await $`tar czf archive ${files}`
   } finally {
     await $`rm archive`
   }
 }
 
-if (test('Scripts with no extension')) {
-  await $`node zx.mjs tests/no-extension`
-  assert.match((await fs.readFile('tests/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')) {
-  await $`node zx.mjs <<< 'require("./package.json").name'`
-}
-
-if (test('Markdown scripts are working')) {
-  await $`node zx.mjs docs/markdown.md`
-}
-
 if (test('Quiet mode is working')) {
-  let {stdout} = await $`node zx.mjs --quiet docs/markdown.md`
-  assert(!stdout.includes('whoami'))
+  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')) {
@@ -131,6 +116,41 @@ if (test('Pipes are working')) {
   }
 }
 
+if (test('question')) {
+  let p = question('foo or bar? ', {choices: ['foo', 'bar']})
+
+  setImmediate(() => {
+    process.stdin.emit('data', 'fo')
+    process.stdin.emit('data', '\t')
+    process.stdin.emit('data', '\n')
+  })
+
+  assert.equal(await p, 'foo')
+}
+
+if (test('ProcessPromise')) {
+  let contents = ''
+  let stream = new Writable({
+    write: function(chunk, encoding, next) {
+      contents += chunk.toString()
+      next()
+    }
+  })
+  let p = $`echo 'test'`.pipe(stream)
+  await p
+  assert(p._piped)
+  assert.equal(contents, 'test\n')
+  assert(p.stderr instanceof Socket)
+
+  let err
+  try {
+    $`echo 'test'`.pipe('str')
+  } catch (p) {
+    err = p
+  }
+  assert.equal(err.message, 'The pipe() method does not take strings. Forgot $?')
+}
+
 if (test('ProcessOutput thrown as error')) {
   let err
   try {
@@ -139,6 +159,8 @@ if (test('ProcessOutput thrown as error')) {
     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'))
 }
 
 if (test('The pipe() throws if already resolved')) {
@@ -174,6 +196,19 @@ if (test('globby available')) {
   assert(typeof globby.isGitIgnored === 'function')
   assert(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'
+  ])
+}
+
+if (test('fetch')) {
+  assert(
+    await fetch('https://example.com'),
+    await fetch('https://example.com', {method: 'GET'})
+  )
 }
 
 if (test('Executes a script from $PATH')) {
@@ -201,6 +236,7 @@ if (test('Executes a script from $PATH')) {
 }
 
 if (test('The cd() works with relative paths')) {
+  let cwd = process.cwd()
   try {
     fs.mkdirpSync('/tmp/zx-cd-test/one/two')
     cd('/tmp/zx-cd-test/one/two')
@@ -216,7 +252,7 @@ if (test('The cd() works with relative paths')) {
     assert.deepEqual(results, ['two', 'one', 'zx-cd-test'])
   } finally {
     fs.rmSync('/tmp/zx-cd-test', {recursive: true})
-    cd(__dirname)
+    cd(cwd)
   }
 }
 
@@ -249,53 +285,8 @@ if (test('which available')) {
   assert.equal(which.sync('npm'), await which('npm'))
 }
 
-if (test('Retry works (experimental)')) {
-  let exitCode = 0
-  let now = Date.now()
-  try {
-    await retry(5, 50)`exit 123`
-  } catch (p) {
-    exitCode = p.exitCode
-  }
-  assert.equal(exitCode, 123)
-  assert(Date.now() >= now + 50 * (5 - 1))
-}
-
-if (test('withTimeout works (experimental)')) {
-  let exitCode = 0
-  let signal
-  try {
-    await withTimeout(100, 'SIGKILL')`sleep 9999`
-  } catch (p) {
-    exitCode = p.exitCode
-    signal = p.signal
-  }
-  assert.equal(exitCode, null)
-  assert.equal(signal, 'SIGKILL')
-}
-
-if (test('echo works (experimental)')) {
-  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 (experimental)')) {
-  let s = startSpinner('waiting')
-
-  await sleep(1000)
-  s()
-}
-
-let version
 if (test('require() is working in ESM')) {
-  let data = require('./package.json')
-  version = data.version
+  let data = require('../package.json')
   assert.equal(data.name, 'zx')
   assert.equal(data, require('zx/package.json'))
 }
-
-console.log('\n' +
-  chalk.black.bgYellowBright(` zx version is ${version} `) + '\n' +
-  chalk.greenBright(` 🍺 ${всегоТестов} tests passed `)
-)
test/test-utils.mjs
@@ -0,0 +1,36 @@
+// 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'
+
+export {strict as assert} from 'assert'
+
+let всегоТестов = 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
+  }
+  return false
+}
+
+export const printTestDigest = () => {
+  console.log('\n' +
+    chalk.black.bgYellowBright(` zx version is ${require('../package.json').version} `) + '\n' +
+    chalk.greenBright(` 🍺 ${всегоТестов} tests passed `)
+  )
+}
test/zx.test.mjs
@@ -0,0 +1,83 @@
+// 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 {assert, test as t} from './test-utils.mjs'
+
+const test = t.bind(null, 'zx')
+
+if (test('supports `-v` flag / prints version')) {
+  let v = (await $`node zx.mjs -v`).toString().trim()
+  assert.equal(v, require('../package.json').version)
+}
+
+if (test('prints help')) {
+  let help
+  try {
+    await $`node zx.mjs`
+  } catch (err) {
+    help = err.toString().trim()
+  }
+  assert(help.includes('print current zx version'))
+}
+
+if (test('supports `--experimental` flag')) {
+  await $`echo 'echo("test")' | node zx.mjs --experimental`
+}
+
+if (test('supports `--quiet` flag / Quiet mode is working')) {
+  let p = await $`node zx.mjs --quiet docs/markdown.md`
+  assert(!p.stdout.includes('whoami'))
+}
+
+if (test('supports `--shell` flag ')) {
+  let shell = $.shell
+  let p = await $`node zx.mjs --shell=${shell} <<< '$\`echo \${$.shell}\`'`
+  assert(p.stdout.includes(shell))
+}
+
+if (test('supports `--prefix` flag ')) {
+  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')) {
+  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`)
+
+  assert(p.stdout.includes('test'))
+  server.kill()
+
+  let err
+  try {
+    await quiet($`node zx.mjs http://127.0.0.1:8081/echo.mjs`)
+  } catch (e) {
+    err = e
+  }
+  assert(err.stderr.includes('ECONNREFUSED'))
+}
+
+if (test('Scripts with no extension')) {
+  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')) {
+  await $`node zx.mjs <<< 'require("./package.json").name'`
+}
+
+if (test('Markdown scripts are working')) {
+  await $`node zx.mjs docs/markdown.md`
+}
.gitignore
@@ -1,3 +1,4 @@
 /node_modules/
 package-lock.json
 yarn.lock
+coverage
package.json
@@ -26,7 +26,10 @@
     "node": ">= 16.0.0"
   },
   "scripts": {
-    "test": "node zx.mjs test.mjs"
+    "test:cov": "c8 --reporter=html npm run test",
+    "test": "node zx.mjs test/full.test.mjs",
+    "test:zx": "npm run test zx",
+    "test:index": "npm run test index"
   },
   "dependencies": {
     "@types/fs-extra": "^9.0.13",