v6
  1// Copyright 2021 Google LLC
  2//
  3// Licensed under the Apache License, Version 2.0 (the "License");
  4// you may not use this file except in compliance with the License.
  5// You may obtain a copy of the License at
  6//
  7//     https://www.apache.org/licenses/LICENSE-2.0
  8//
  9// Unless required by applicable law or agreed to in writing, software
 10// distributed under the License is distributed on an "AS IS" BASIS,
 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12// See the License for the specific language governing permissions and
 13// limitations under the License.
 14
 15import { test } from 'uvu'
 16import * as assert from 'uvu/assert'
 17import { inspect } from 'node:util'
 18import chalk from 'chalk'
 19import { Writable } from 'node:stream'
 20import { Socket } from 'node:net'
 21import '../build/globals.js'
 22import { ProcessPromise } from '../build/index.js'
 23import { getCtx, runInCtx } from '../build/context.js'
 24
 25$.verbose = false
 26
 27test('only stdout is used during command substitution', async () => {
 28  let hello = await $`echo Error >&2; echo Hello`
 29  let len = +(await $`echo ${hello} | wc -c`)
 30  assert.is(len, 6)
 31})
 32
 33test('env vars works', async () => {
 34  process.env.ZX_TEST_FOO = 'foo'
 35  let foo = await $`echo $ZX_TEST_FOO`
 36  assert.is(foo.stdout, 'foo\n')
 37})
 38
 39test('env vars is safe to pass', async () => {
 40  process.env.ZX_TEST_BAR = 'hi; exit 1'
 41  await $`echo $ZX_TEST_BAR`
 42})
 43
 44test('arguments are quoted', async () => {
 45  let bar = 'bar"";baz!$#^$\'&*~*%)({}||\\/'
 46  assert.is((await $`echo ${bar}`).stdout.trim(), bar)
 47})
 48
 49test('undefined and empty string correctly quoted', async () => {
 50  $.verbose = true
 51  assert.is((await $`echo -n ${undefined}`).toString(), 'undefined')
 52  assert.is((await $`echo -n ${''}`).toString(), '')
 53  $.verbose = false
 54})
 55
 56test('can create a dir with a space in the name', async () => {
 57  let name = 'foo bar'
 58  try {
 59    await $`mkdir /tmp/${name}`
 60  } catch {
 61    assert.unreachable()
 62  } finally {
 63    await fs.rmdir('/tmp/' + name)
 64  }
 65})
 66
 67test('pipefail is on', async () => {
 68  let p
 69  try {
 70    p = await $`cat /dev/not_found | sort`
 71  } catch (e) {
 72    console.log('Caught an exception -> ok')
 73    p = e
 74  }
 75  assert.is.not(p.exitCode, 0)
 76})
 77
 78test('toString() is called on arguments', async () => {
 79  let foo = 0
 80  let p = await $`echo ${foo}`
 81  assert.is(p.stdout, '0\n')
 82})
 83
 84test('can use array as an argument', async () => {
 85  let args = ['-n', 'foo']
 86  assert.is((await $`echo ${args}`).toString(), 'foo')
 87})
 88
 89test('quiet mode is working', async () => {
 90  let stdout = ''
 91  let log = console.log
 92  console.log = (...args) => {
 93    stdout += args.join(' ')
 94  }
 95  await quiet($`echo 'test'`)
 96  console.log = log
 97  assert.is(stdout, '')
 98})
 99
100test('pipes are working', async () => {
101  let { stdout } = await $`echo "hello"`
102    .pipe($`awk '{print $1" world"}'`)
103    .pipe($`tr '[a-z]' '[A-Z]'`)
104  assert.is(stdout, 'HELLO WORLD\n')
105
106  try {
107    await $`echo foo`.pipe(fs.createWriteStream('/tmp/output.txt'))
108    assert.is((await fs.readFile('/tmp/output.txt')).toString(), 'foo\n')
109
110    let r = $`cat`
111    fs.createReadStream('/tmp/output.txt').pipe(r.stdin)
112    assert.is((await r).stdout, 'foo\n')
113  } finally {
114    await fs.rm('/tmp/output.txt')
115  }
116})
117
118test('question', async () => {
119  let p = question('foo or bar? ', { choices: ['foo', 'bar'] })
120
121  setImmediate(() => {
122    process.stdin.emit('data', 'fo')
123    process.stdin.emit('data', '\t')
124    process.stdin.emit('data', '\n')
125  })
126
127  assert.is(await p, 'foo')
128})
129
130test('ProcessPromise', async () => {
131  let contents = ''
132  let stream = new Writable({
133    write: function (chunk, encoding, next) {
134      contents += chunk.toString()
135      next()
136    },
137  })
138  let p = $`echo 'test'`.pipe(stream)
139  await p
140  assert.ok(p._piped)
141  assert.is(contents, 'test\n')
142  assert.instance(p.stderr, Socket)
143
144  let err
145  try {
146    $`echo 'test'`.pipe('str')
147  } catch (p) {
148    err = p
149  }
150  assert.is(err.message, 'The pipe() method does not take strings. Forgot $?')
151})
152
153test('ProcessPromise: inherits native Promise', async () => {
154  const p1 = $`echo 1`
155  const p2 = p1.then((v) => v)
156  const p3 = p2.then((v) => v)
157  const p4 = p3.catch((v) => v)
158  const p5 = p1.finally((v) => v)
159
160  assert.instance(p1, Promise)
161  assert.instance(p1, ProcessPromise)
162  assert.instance(p2, ProcessPromise)
163  assert.instance(p3, ProcessPromise)
164  assert.instance(p4, ProcessPromise)
165  assert.instance(p5, ProcessPromise)
166  assert.ok(p1 !== p2)
167  assert.ok(p2 !== p3)
168  assert.ok(p3 !== p4)
169  assert.ok(p5 !== p1)
170
171  assert.ok(p1.ctx)
172  assert.ok(p2.ctx)
173  assert.ok(p3.ctx)
174  assert.ok(p4.ctx)
175  assert.ok(p5.ctx)
176
177  assert.not.equal(p1.ctx, p2.ctx)
178  assert.equal(p2.ctx, p3.ctx)
179  assert.equal(p3.ctx, p4.ctx)
180  assert.equal(p4.ctx, p5.ctx)
181})
182
183test('ProcessPromise: ctx is protected from removal', async () => {
184  const p = $`echo 1`
185
186  try {
187    delete p.ctx
188    assert.unreachable()
189  } catch (e) {
190    assert.match(e.message, /Cannot delete property/)
191  }
192})
193
194test('ProcessOutput thrown as error', async () => {
195  let err
196  try {
197    await $`unknown`
198  } catch (p) {
199    err = p
200  }
201  assert.ok(err.exitCode > 0)
202  assert.match(err.stderr, /unknown: command not found/)
203  assert.ok(err[inspect.custom]().includes('Command not found'))
204})
205
206test('pipe() throws if already resolved', async (t) => {
207  let out,
208    p = $`echo "Hello"`
209  await p
210  try {
211    out = await p.pipe($`less`)
212  } catch (err) {
213    assert.is(
214      err.message,
215      `The pipe() method shouldn't be called after promise is already resolved!`
216    )
217  }
218  if (out) {
219    t.fail('Expected failure!')
220  }
221})
222
223test('await $`cmd`.exitCode does not throw', async () => {
224  assert.is.not(await $`grep qwerty README.md`.exitCode, 0)
225  assert.is(await $`[[ -f README.md ]]`.exitCode, 0)
226})
227
228test('nothrow() do not throw', async () => {
229  let { exitCode } = await nothrow($`exit 42`)
230  assert.is(exitCode, 42)
231})
232
233test('globby available', async () => {
234  assert.is(globby, glob)
235  assert.is(typeof globby, 'function')
236  assert.is(typeof globby.globbySync, 'function')
237  assert.is(typeof globby.globbyStream, 'function')
238  assert.is(typeof globby.generateGlobTasks, 'function')
239  assert.is(typeof globby.isDynamicPattern, 'function')
240  assert.is(typeof globby.isGitIgnored, 'function')
241  assert.is(typeof globby.isGitIgnoredSync, 'function')
242  assert.equal(await globby('*.md'), ['README.md'])
243})
244
245test('fetch', async () => {
246  assert.match(
247    await fetch('https://medv.io').then((res) => res.text()),
248    /Anton Medvedev/
249  )
250})
251
252test('executes a script from $PATH', async () => {
253  const isWindows = process.platform === 'win32'
254  const oldPath = process.env.PATH
255
256  const envPathSeparator = isWindows ? ';' : ':'
257  process.env.PATH += envPathSeparator + path.resolve('/tmp/')
258
259  const toPOSIXPath = (_path) => _path.split(path.sep).join(path.posix.sep)
260
261  const zxPath = path.resolve('./build/cli.js')
262  const zxLocation = isWindows ? toPOSIXPath(zxPath) : zxPath
263  const scriptCode = `#!/usr/bin/env ${zxLocation}\nconsole.log('The script from path runs.')`
264
265  try {
266    await $`chmod +x ${zxLocation}`
267    await $`echo ${scriptCode}`.pipe(
268      fs.createWriteStream('/tmp/script-from-path', { mode: 0o744 })
269    )
270    await $`script-from-path`
271  } finally {
272    process.env.PATH = oldPath
273    fs.rmSync('/tmp/script-from-path')
274  }
275})
276
277test('cd() works with relative paths', async () => {
278  let cwd = process.cwd()
279  assert.equal($.cwd, cwd)
280  try {
281    fs.mkdirpSync('/tmp/zx-cd-test/one/two')
282    cd('/tmp/zx-cd-test/one/two')
283    let p1 = $`pwd`
284    assert.ok($.cwd.endsWith('/two'))
285    assert.ok(process.cwd().endsWith('/two'))
286
287    cd('..')
288    let p2 = $`pwd`
289    assert.ok($.cwd.endsWith('/one'))
290    assert.ok(process.cwd().endsWith('/one'))
291
292    cd('..')
293    let p3 = $`pwd`
294    assert.ok(process.cwd().endsWith('/zx-cd-test'))
295    assert.ok($.cwd.endsWith('/tmp/zx-cd-test'))
296
297    let results = (await Promise.all([p1, p2, p3])).map((p) =>
298      path.basename(p.stdout.trim())
299    )
300
301    assert.equal(results, ['two', 'one', 'zx-cd-test'])
302  } catch (e) {
303    assert.ok(!e, e)
304  } finally {
305    fs.rmSync('/tmp/zx-cd-test', { recursive: true })
306    cd(cwd)
307    assert.equal($.cwd, cwd)
308  }
309})
310
311test('cd() does not affect parallel contexts', async () => {
312  let cwd = process.cwd()
313  let resolve, reject
314  let promise = new ProcessPromise((...args) => ([resolve, reject] = args))
315
316  try {
317    fs.mkdirpSync('/tmp/zx-cd-parallel')
318    runInCtx({ ...getCtx() }, async () => {
319      assert.equal($.cwd, cwd)
320      await sleep(10)
321      cd('/tmp/zx-cd-parallel')
322      assert.ok(getCtx().cwd.endsWith('/zx-cd-parallel'))
323      assert.ok($.cwd.endsWith('/zx-cd-parallel'))
324    })
325
326    runInCtx({ ...getCtx() }, async () => {
327      assert.equal($.cwd, cwd)
328      assert.equal(getCtx().cwd, cwd)
329      await sleep(20)
330      assert.equal(getCtx().cwd, cwd)
331      assert.ok($.cwd.endsWith('/zx-cd-parallel'))
332      resolve()
333    })
334
335    await promise
336  } catch (e) {
337    assert.ok(!e, e)
338  } finally {
339    fs.rmSync('/tmp/zx-cd-parallel', { recursive: true })
340    cd(cwd)
341  }
342})
343
344test('kill() method works', async () => {
345  let p = nothrow($`sleep 9999`)
346  setTimeout(() => {
347    p.kill()
348  }, 100)
349  await p
350})
351
352test('a signal is passed with kill() method', async () => {
353  let p = $`while true; do :; done`
354  setTimeout(() => p.kill('SIGKILL'), 100)
355  let signal
356  try {
357    await p
358  } catch (p) {
359    signal = p.signal
360  }
361  assert.equal(signal, 'SIGKILL')
362})
363
364test('YAML works', async () => {
365  assert.equal(YAML.parse(YAML.stringify({ foo: 'bar' })), { foo: 'bar' })
366})
367
368test('which available', async () => {
369  assert.is(which.sync('npm'), await which('npm'))
370})
371
372test.run()