v7
  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 chalk from 'chalk'
 16import { suite } from 'uvu'
 17import * as assert from 'uvu/assert'
 18import { inspect } from 'node:util'
 19import { Writable } from 'node:stream'
 20import { Socket } from 'node:net'
 21import { ProcessPromise, ProcessOutput } from '../build/index.js'
 22import '../build/globals.js'
 23
 24const test = suite('core')
 25
 26$.verbose = false
 27
 28test('only stdout is used during command substitution', async () => {
 29  let hello = await $`echo Error >&2; echo Hello`
 30  let len = +(await $`echo ${hello} | wc -c`)
 31  assert.is(len, 6)
 32})
 33
 34test('env vars works', async () => {
 35  process.env.ZX_TEST_FOO = 'foo'
 36  let foo = await $`echo $ZX_TEST_FOO`
 37  assert.is(foo.stdout, 'foo\n')
 38})
 39
 40test('env vars is safe to pass', async () => {
 41  process.env.ZX_TEST_BAR = 'hi; exit 1'
 42  await $`echo $ZX_TEST_BAR`
 43})
 44
 45test('arguments are quoted', async () => {
 46  let bar = 'bar"";baz!$#^$\'&*~*%)({}||\\/'
 47  assert.is((await $`echo ${bar}`).stdout.trim(), bar)
 48})
 49
 50test('undefined and empty string correctly quoted', async () => {
 51  assert.is((await $`echo -n ${undefined}`).toString(), 'undefined')
 52  assert.is((await $`echo -n ${''}`).toString(), '')
 53})
 54
 55test('can create a dir with a space in the name', async () => {
 56  let name = 'foo bar'
 57  try {
 58    await $`mkdir /tmp/${name}`
 59  } catch {
 60    assert.unreachable()
 61  } finally {
 62    await fs.rmdir('/tmp/' + name)
 63  }
 64})
 65
 66test('pipefail is on', async () => {
 67  let p
 68  try {
 69    p = await $`cat /dev/not_found | sort`
 70  } catch (e) {
 71    p = e
 72  }
 73  assert.is.not(p.exitCode, 0)
 74})
 75
 76test('toString() is called on arguments', async () => {
 77  let foo = 0
 78  let p = await $`echo ${foo}`
 79  assert.is(p.stdout, '0\n')
 80})
 81
 82test('can use array as an argument', async () => {
 83  let args = ['-n', 'foo']
 84  assert.is((await $`echo ${args}`).toString(), 'foo')
 85})
 86
 87test('quiet() mode is working', async () => {
 88  let stdout = ''
 89  let log = console.log
 90  console.log = (...args) => {
 91    stdout += args.join(' ')
 92  }
 93  await $`echo 'test'`.quiet()
 94  console.log = log
 95  assert.is(stdout, '')
 96  {
 97    // Deprecated.
 98    let stdout = ''
 99    let log = console.log
100    console.log = (...args) => {
101      stdout += args.join(' ')
102    }
103    await quiet($`echo 'test'`)
104    console.log = log
105    assert.is(stdout, '')
106  }
107})
108
109test('pipes are working', async () => {
110  let { stdout } = await $`echo "hello"`
111    .pipe($`awk '{print $1" world"}'`)
112    .pipe($`tr '[a-z]' '[A-Z]'`)
113  assert.is(stdout, 'HELLO WORLD\n')
114
115  try {
116    await $`echo foo`.pipe(fs.createWriteStream('/tmp/output.txt'))
117    assert.is((await fs.readFile('/tmp/output.txt')).toString(), 'foo\n')
118
119    let r = $`cat`
120    fs.createReadStream('/tmp/output.txt').pipe(r.stdin)
121    assert.is((await r).stdout, 'foo\n')
122  } finally {
123    await fs.rm('/tmp/output.txt')
124  }
125})
126
127test('ProcessPromise', async () => {
128  let contents = ''
129  let stream = new Writable({
130    write: function (chunk, encoding, next) {
131      contents += chunk.toString()
132      next()
133    },
134  })
135  let p = $`echo 'test'`.pipe(stream)
136  await p
137  assert.ok(p._piped)
138  assert.is(contents, 'test\n')
139  assert.instance(p.stderr, Socket)
140
141  let err
142  try {
143    $`echo 'test'`.pipe('str')
144  } catch (p) {
145    err = p
146  }
147  assert.is(err.message, 'The pipe() method does not take strings. Forgot $?')
148})
149
150test('ProcessPromise: inherits native Promise', async () => {
151  const p1 = $`echo 1`
152  const p2 = p1.then((v) => v)
153  const p3 = p2.then((v) => v)
154  const p4 = p3.catch((v) => v)
155  const p5 = p1.finally((v) => v)
156
157  assert.instance(p1, Promise)
158  assert.instance(p1, ProcessPromise)
159  assert.instance(p2, ProcessPromise)
160  assert.instance(p3, ProcessPromise)
161  assert.instance(p4, ProcessPromise)
162  assert.instance(p5, ProcessPromise)
163  assert.ok(p1 !== p2)
164  assert.ok(p2 !== p3)
165  assert.ok(p3 !== p4)
166  assert.ok(p5 !== p1)
167})
168
169test('cd() works with relative paths', async () => {
170  let cwd = process.cwd()
171  try {
172    fs.mkdirpSync('/tmp/zx-cd-test/one/two')
173    cd('/tmp/zx-cd-test/one/two')
174    let p1 = $`pwd`
175    assert.is($.cwd, undefined)
176    assert.match(process.cwd(), '/two')
177
178    cd('..')
179    let p2 = $`pwd`
180    assert.is($.cwd, undefined)
181    assert.match(process.cwd(), '/one')
182
183    cd('..')
184    let p3 = $`pwd`
185    assert.is($.cwd, undefined)
186    assert.match(process.cwd(), '/tmp/zx-cd-test')
187
188    const results = (await Promise.all([p1, p2, p3])).map((p) =>
189      path.basename(p.stdout.trim())
190    )
191    assert.equal(results, ['two', 'one', 'zx-cd-test'])
192  } catch (e) {
193    assert.ok(!e, e)
194  } finally {
195    fs.rmSync('/tmp/zx-cd-test', { recursive: true })
196    cd(cwd)
197  }
198})
199
200test('cd() does affect parallel contexts', async () => {
201  const cwd = process.cwd()
202  try {
203    fs.mkdirpSync('/tmp/zx-cd-parallel/one/two')
204    await Promise.all([
205      within(async () => {
206        assert.is(process.cwd(), cwd)
207        await sleep(1)
208        cd('/tmp/zx-cd-parallel/one')
209        assert.match(process.cwd(), '/tmp/zx-cd-parallel/one')
210      }),
211      within(async () => {
212        assert.is(process.cwd(), cwd)
213        await sleep(2)
214        assert.is(process.cwd(), cwd)
215      }),
216      within(async () => {
217        assert.is(process.cwd(), cwd)
218        await sleep(3)
219        $.cwd = '/tmp/zx-cd-parallel/one/two'
220        assert.is(process.cwd(), cwd)
221        assert.match((await $`pwd`).stdout, '/tmp/zx-cd-parallel/one/two')
222      }),
223    ])
224  } catch (e) {
225    assert.ok(!e, e)
226  } finally {
227    fs.rmSync('/tmp/zx-cd-parallel', { recursive: true })
228    cd(cwd)
229  }
230})
231
232test('cd() fails on entering not existing dir', async () => {
233  assert.throws(() => cd('/tmp/abra-kadabra'))
234})
235
236test('cd() accepts ProcessOutput in addition to string', async () => {
237  within(async () => {
238    const tmpDir = await $`mktemp -d`
239    cd(tmpDir)
240    assert.match(process.cwd(), tmpDir.toString().trimEnd())
241  })
242})
243
244test('kill() method works', async () => {
245  let p = $`sleep 9999`.nothrow()
246  setTimeout(() => {
247    p.kill()
248  }, 100)
249  await p
250})
251
252test('a signal is passed with kill() method', async () => {
253  let p = $`while true; do :; done`
254  setTimeout(() => p.kill('SIGKILL'), 100)
255  let signal
256  try {
257    await p
258  } catch (p) {
259    signal = p.signal
260  }
261  assert.equal(signal, 'SIGKILL')
262})
263
264test('within() works', async () => {
265  let resolve, reject
266  let promise = new Promise((...args) => ([resolve, reject] = args))
267
268  function yes() {
269    assert.equal($.verbose, true)
270    resolve()
271  }
272
273  $.verbose = false
274  assert.equal($.verbose, false)
275
276  within(() => {
277    $.verbose = true
278  })
279  assert.equal($.verbose, false)
280
281  within(async () => {
282    $.verbose = true
283    setTimeout(yes, 10)
284  })
285  assert.equal($.verbose, false)
286
287  await promise
288})
289
290test('within() restores previous cwd', async () => {
291  let resolve, reject
292  let promise = new Promise((...args) => ([resolve, reject] = args))
293
294  let pwd = await $`pwd`
295
296  within(async () => {
297    $.verbose = false
298    cd('/tmp')
299    setTimeout(async () => {
300      assert.match((await $`pwd`).stdout, '/tmp')
301      resolve()
302    }, 1000)
303  })
304
305  assert.equal((await $`pwd`).stdout, pwd.stdout)
306  await promise
307})
308
309test(`within() isolates nested context and returns cb result`, async () => {
310  within(async () => {
311    const res = await within(async () => {
312      $.verbose = true
313
314      return within(async () => {
315        assert.equal($.verbose, true)
316        $.verbose = false
317
318        return within(async () => {
319          assert.equal($.verbose, false)
320          $.verbose = true
321          return 'foo'
322        })
323      })
324    })
325    assert.equal($.verbose, false)
326    assert.equal(res, 'foo')
327  })
328})
329
330test('stdio() works', async () => {
331  let p = $`printf foo`
332  await p
333  assert.throws(() => p.stdin)
334  assert.is((await p).stdout, 'foo')
335
336  let b = $`read; printf $REPLY`
337  b.stdin.write('bar\n')
338  assert.is((await b).stdout, 'bar')
339})
340
341test('snapshots works', async () => {
342  await within(async () => {
343    $.prefix += 'echo success;'
344    let p = $`:`
345    $.prefix += 'echo fail;'
346    let out = await p
347    assert.is(out.stdout, 'success\n')
348    assert.not.match(out.stdout, 'fail')
349  })
350})
351
352test('timeout() works', async () => {
353  let exitCode, signal
354  try {
355    await $`sleep 9999`.timeout(10, 'SIGKILL')
356  } catch (p) {
357    exitCode = p.exitCode
358    signal = p.signal
359  }
360  assert.is(exitCode, null)
361  assert.is(signal, 'SIGKILL')
362})
363
364test('timeout() expiration works', async () => {
365  let exitCode, signal
366  try {
367    await $`sleep 1`.timeout(999)
368  } catch (p) {
369    exitCode = p.exitCode
370    signal = p.signal
371  }
372  assert.is(exitCode, undefined)
373  assert.is(signal, undefined)
374})
375
376test('$ thrown as error', async () => {
377  let err
378  try {
379    await $`wtf`
380  } catch (p) {
381    err = p
382  }
383  assert.ok(err.exitCode > 0)
384  assert.ok(err.stderr.includes('wtf: command not found'))
385  assert.ok(err[inspect.custom]().includes('Command not found'))
386})
387
388test('error event is handled', async () => {
389  await within(async () => {
390    $.cwd = 'wtf'
391    try {
392      await $`pwd`
393      assert.unreachable('should have thrown')
394    } catch (err) {
395      assert.instance(err, ProcessOutput)
396      assert.match(err.message, /No such file or directory/)
397    }
398  })
399})
400
401test('pipe() throws if already resolved', async (t) => {
402  let ok = true
403  let p = $`echo "Hello"`
404  await p
405  try {
406    await p.pipe($`less`)
407    ok = false
408  } catch (err) {
409    assert.is(
410      err.message,
411      `The pipe() method shouldn't be called after promise is already resolved!`
412    )
413  }
414  assert.ok(ok, 'Expected failure!')
415})
416
417test('await $`cmd`.exitCode does not throw', async () => {
418  assert.is.not(await $`grep qwerty README.md`.exitCode, 0)
419  assert.is(await $`[[ -f README.md ]]`.exitCode, 0)
420})
421
422test('nothrow() do not throw', async () => {
423  let { exitCode } = await $`exit 42`.nothrow()
424  assert.is(exitCode, 42)
425  {
426    // Deprecated.
427    let { exitCode } = await nothrow($`exit 42`)
428    assert.is(exitCode, 42)
429  }
430})
431
432test('malformed cmd error', async () => {
433  assert.throws(() => $`\033`, /malformed/i)
434})
435
436test('$ is a regular function', async () => {
437  const _$ = $.bind(null)
438  let foo = await _$`echo foo`
439  assert.is(foo.stdout, 'foo\n')
440  assert.ok(typeof $.call === 'function')
441  assert.ok(typeof $.apply === 'function')
442})
443
444test('halt() works', async () => {
445  let filepath = `/tmp/${Math.random().toString()}`
446  let p = $`touch ${filepath}`.halt()
447  await sleep(1)
448  assert.not.ok(
449    fs.existsSync(filepath),
450    'The cmd called, but it should not have been called'
451  )
452  await p.run()
453  assert.ok(fs.existsSync(filepath), 'The cmd should have been called')
454})
455
456test('await on halted throws', async () => {
457  let p = $`sleep 1`.halt()
458  let ok = true
459  try {
460    await p
461    ok = false
462  } catch (err) {
463    assert.is(err.message, 'The process is halted!')
464  }
465  assert.ok(ok, 'Expected failure!')
466})
467
468test.run()