main
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 assert from 'node:assert'
16import { test, describe, before, after, it } from 'node:test'
17import { inspect } from 'node:util'
18import { basename } from 'node:path'
19import { WriteStream } from 'node:fs'
20import { Readable, Transform, Writable } from 'node:stream'
21import { Socket } from 'node:net'
22import { ChildProcess } from 'node:child_process'
23import {
24 $,
25 ProcessPromise,
26 ProcessOutput,
27 defaults,
28 resolveDefaults,
29 cd,
30 syncProcessCwd,
31 within,
32 usePowerShell,
33 usePwsh,
34 useBash,
35 Fail,
36 kill,
37} from '../build/core.js'
38import {
39 tempfile,
40 tempdir,
41 fs,
42 quote,
43 quotePowerShell,
44 sleep,
45 quiet,
46 which,
47 nothrow,
48 fetch,
49} from '../build/index.js'
50import { noop } from '../build/util.cjs'
51import { EventEmitter } from 'node:events'
52
53describe('core', () => {
54 describe('resolveDefaults()', () => {
55 test('overrides known (allowed) opts', async () => {
56 const defaults = resolveDefaults({ verbose: false }, 'ZX_', {
57 ZX_VERBOSE: 'true',
58 ZX_PREFER_LOCAL: '/foo/bar/',
59 })
60 assert.equal(defaults.verbose, true)
61 assert.equal(defaults.preferLocal, '/foo/bar/')
62 })
63
64 test('ignores unknown', async () => {
65 const defaults = resolveDefaults({}, 'ZX_', {
66 ZX_INPUT: 'input',
67 ZX_FOO: 'test',
68 })
69 assert.equal(defaults.input, undefined)
70 assert.equal(defaults.foo, undefined)
71 })
72 })
73
74 describe('$', () => {
75 test('is a regular function', async () => {
76 const _$ = $.bind(null)
77 const foo = await _$`echo foo`
78 assert.equal(foo.stdout, 'foo\n')
79 assert.ok(typeof $.call === 'function')
80 assert.ok(typeof $.apply === 'function')
81 })
82
83 test('only stdout is used during command substitution', async () => {
84 const hello = await $`echo Error >&2; echo Hello`
85 const len = +(await $`echo ${hello} | wc -c`)
86 assert.equal(len, 6)
87 })
88
89 test('env vars works', async () => {
90 process.env.ZX_TEST_FOO = 'foo'
91 const foo = await $`echo $ZX_TEST_FOO`
92 assert.equal(foo.stdout, 'foo\n')
93 delete process.env.ZX_TEST_FOO
94 })
95
96 test('env vars are safe to pass', async () => {
97 process.env.ZX_TEST_BAR = 'hi; exit 1'
98 const bar = await $`echo $ZX_TEST_BAR`
99 assert.equal(bar.stdout, 'hi; exit 1\n')
100 delete process.env.ZX_TEST_BAR
101 })
102
103 test('arguments are quoted', async () => {
104 const bar = 'bar"";baz!$#^$\'&*~*%)({}||\\/'
105 assert.equal((await $`echo ${bar}`).stdout.trim(), bar)
106 })
107
108 test('broken quoting', async () => {
109 const args = ['param && echo bar']
110 const p = $`echo --foo=$'${args}'`
111 assert.equal((await p).stdout, '--foo=$param\nbar\n')
112 })
113
114 test('undefined and empty string correctly quoted', async () => {
115 assert.equal((await $`echo -n ${undefined}`).toString(), 'undefined')
116 assert.equal((await $`echo -n ${''}`).toString(), '')
117 })
118
119 test('accepts thenable arguments', async () => {
120 const a1 = $`echo foo`
121 const a2 = new Promise((res) => setTimeout(res, 10, ['bar', 'baz']))
122 const a3 = new Promise((_, rej) => setTimeout(rej, 20, 'failure'))
123
124 const p1 = $`echo ${a1} ${a2}`
125 assert(p1.cmd instanceof Promise)
126 const o1 = await p1
127 assert(o1 instanceof ProcessOutput)
128 assert.equal(o1.stdout.trim(), 'foo bar baz')
129 assert.equal(p1.cmd, 'echo foo bar baz')
130
131 try {
132 await $`echo ${a3}`
133 } catch (e) {
134 assert.ok(e instanceof ProcessOutput)
135 assert.equal(e.exitCode, null)
136 assert.equal(e.cause, 'failure')
137 }
138
139 try {
140 await $`echo ${$`exit 1`}`
141 } catch (e) {
142 assert.ok(e instanceof ProcessOutput)
143 assert.ok(e.cause instanceof ProcessOutput)
144 assert.equal(e.exitCode, null)
145 assert.equal(e.cause.exitCode, 1)
146 }
147
148 await Promise.allSettled([a3])
149 })
150
151 test.skip('handles multiline literals', async () => {
152 assert.equal(
153 (
154 await $`echo foo
155 bar
156 "baz
157 qux"
158`
159 ).toString(),
160 'foo bar baz\n qux\n'
161 )
162 assert.equal(
163 (
164 await $`echo foo \
165 bar \
166 baz \
167`
168 ).toString(),
169 'foo bar baz\n'
170 )
171 })
172
173 test('can create a dir with a space in the name', async () => {
174 const name = 'foo bar'
175 try {
176 await $`mkdir /tmp/${name}`
177 } catch {
178 assert.unreachable()
179 } finally {
180 await fs.rmdir('/tmp/' + name)
181 }
182 })
183
184 test('pipefail is on', async () => {
185 let p
186 try {
187 p = await $`cat /dev/not_found | sort`
188 } catch (e) {
189 p = e
190 }
191 assert.notEqual(p.exitCode, 0)
192 })
193
194 test('toString() is called on arguments', async () => {
195 const foo = 0
196 const p = await $`echo ${foo}`
197 assert.equal(p.stdout, '0\n')
198 })
199
200 test('can use array as an argument', async () => {
201 const _$ = $({ prefix: '', postfix: '' })
202 const p1 = _$`echo ${['-n', 'foo']}`
203 assert.equal(p1.cmd, 'echo -n foo')
204 assert.equal((await p1).toString(), 'foo')
205
206 const p2 = _$`echo ${[1, '', '*', '2']}`
207 assert.equal(p2.cmd, `echo 1 $'' $'*' 2`)
208 assert.equal((await p2).toString(), `1 * 2\n`)
209 })
210
211 test('requires $.shell to be specified', async () => {
212 await within(() => {
213 $.shell = undefined
214 assert.throws(() => $`echo foo`, /shell/)
215 })
216 })
217
218 test('malformed cmd error', async () => {
219 assert.throws(() => $`\033`, /malformed/i)
220
221 try {
222 $([null])
223 throw new Error('unreachable')
224 } catch (e) {
225 assert.ok(e instanceof Fail)
226 assert.match(e.message, /malformed/i)
227 }
228
229 const o = await $({ nothrow: true })`\033`
230 assert.equal(o.ok, false)
231 assert.match(o.cause.message, /malformed/i)
232 })
233
234 test('snapshots works', async () => {
235 await within(async () => {
236 $.prefix += 'echo success;'
237 const p = $`:`
238 $.prefix += 'echo fail;'
239 const out = await p
240 assert.equal(out.stdout, 'success\n')
241 assert.doesNotMatch(out.stdout, /fail/)
242 })
243 })
244
245 test('$ thrown as error', async () => {
246 let err
247 try {
248 await $`wtf`
249 } catch (p) {
250 err = p
251 }
252 assert.ok(err.exitCode > 0)
253 assert.match(err.toString(), /command not found/)
254 assert.match(err.valueOf(), /command not found/)
255 assert.match(err.stderr, /wtf: command not found/)
256 assert.match(err[inspect.custom](), /Command not found/)
257 })
258
259 test('error event is handled', async () => {
260 await within(async () => {
261 $.cwd = 'wtf'
262 try {
263 await $`pwd`
264 assert.unreachable('should have thrown')
265 } catch (err) {
266 assert.ok(err instanceof ProcessOutput)
267 assert.match(err.message, /No such file or directory/)
268 }
269 })
270 })
271
272 test('await $`cmd`.exitCode does not throw', async () => {
273 assert.notEqual(await $`grep qwerty README.md`.exitCode, 0)
274 assert.equal(await $`[[ -f README.md ]]`.exitCode, 0)
275 })
276
277 test('`$.sync()` provides synchronous API', () => {
278 const o1 = $.sync`echo foo`
279 const o2 = $({ sync: true })`echo foo`
280 const o3 = $.sync({})`echo foo`
281 assert.equal(o1.stdout, 'foo\n')
282 assert.equal(o2.stdout, 'foo\n')
283 assert.equal(o3.stdout, 'foo\n')
284 })
285
286 describe('$({opts}) API', () => {
287 it('$ proxy uses `defaults` store', () => {
288 assert.equal($.foo, undefined)
289 defaults.foo = 'bar'
290 $.baz = 'qux'
291 assert.equal($.foo, 'bar')
292 assert.equal($.baz, 'qux')
293 assert.equal(defaults.baz, 'qux')
294 delete defaults.foo
295 $.baz = undefined
296 assert.equal($.foo, undefined)
297 assert.equal($.baz, undefined)
298 assert.equal(defaults.baz, undefined)
299 })
300
301 test('provides presets', async () => {
302 const $1 = $({ nothrow: true })
303 assert.equal((await $1`exit 1`).exitCode, 1)
304
305 const $2 = $1({ sync: true })
306 assert.equal($2`exit 2`.exitCode, 2)
307
308 const $3 = $({ sync: true })({ nothrow: true })
309 assert.equal($3`exit 3`.exitCode, 3)
310 })
311
312 test('handles `nothrow` option', async () => {
313 const o1 = await $({ nothrow: true })`exit 1`
314 assert.equal(o1.ok, false)
315 assert.equal(o1.exitCode, 1)
316 assert.match(o1.message, /exit code: 1/)
317
318 const err = new Error('BrokenSpawn')
319 const o2 = await $({
320 nothrow: true,
321 spawn() {
322 throw err
323 },
324 })`echo foo`
325 assert.equal(o2.ok, false)
326 assert.equal(o2.exitCode, null)
327 assert.match(o2.message, /BrokenSpawn/)
328 assert.equal(o2.cause, err)
329 })
330
331 test('handles `input` option', async () => {
332 const p1 = $({ input: 'foo' })`cat`
333 const p2 = $({ input: Readable.from('bar') })`cat`
334 const p3 = $({ input: Buffer.from('baz') })`cat`
335 const p4 = $({ input: p3 })`cat`
336 const p5 = $({ input: await p3 })`cat`
337
338 assert.equal((await p1).stdout, 'foo')
339 assert.equal((await p2).stdout, 'bar')
340 assert.equal((await p3).stdout, 'baz')
341 assert.equal((await p4).stdout, 'baz')
342 assert.equal((await p5).stdout, 'baz')
343 })
344
345 test('handles `timeout` and `timeoutSignal`', async () => {
346 let exitCode, signal
347 try {
348 await $({
349 timeout: 10,
350 timeoutSignal: 'SIGKILL',
351 })`sleep 999`
352 } catch (p) {
353 exitCode = p.exitCode
354 signal = p.signal
355 }
356 assert.equal(exitCode, null)
357 assert.equal(signal, 'SIGKILL')
358 })
359
360 test('`env` option', async () => {
361 const baz = await $({
362 env: { ZX_TEST_BAZ: 'baz' },
363 })`echo $ZX_TEST_BAZ`
364 assert.equal(baz.stdout, 'baz\n')
365 })
366
367 test('`preferLocal` preserves env', async () => {
368 const cases = [
369 [true, `${process.cwd()}/node_modules/.bin:${process.cwd()}:`],
370 ['/foo', `/foo/node_modules/.bin:/foo:`],
371 [
372 ['/bar', '/baz'],
373 `/bar/node_modules/.bin:/bar:/baz/node_modules/.bin:/baz`,
374 ],
375 ]
376
377 for (const [preferLocal, expected] of cases) {
378 const PATH = await $({
379 preferLocal,
380 env: { PATH: process.env.PATH },
381 })`echo $PATH`
382 assert(PATH.stdout.startsWith(expected))
383 }
384 })
385
386 test('supports custom intermediate store', async () => {
387 const getFixedSizeArray = (size) => {
388 const arr = []
389 return new Proxy(arr, {
390 get: (target, prop) =>
391 prop === 'push' && arr.length >= size
392 ? () => {
393 /* noop */
394 }
395 : target[prop],
396 })
397 }
398 const store = {
399 stdout: getFixedSizeArray(1),
400 stderr: getFixedSizeArray(1),
401 stdall: getFixedSizeArray(0),
402 }
403
404 const p = await $({ store })`echo foo`
405
406 assert.equal(p.stdout.trim(), 'foo')
407 assert.equal(p.toString(), '')
408 })
409 })
410
411 describe('accepts `stdio`', () => {
412 test('ignore', async () => {
413 const p = $({ stdio: 'ignore' })`echo foo`
414 assert.equal((await p).stdout, '')
415 })
416
417 test('inherit', async () => {
418 const r1 = (await $({ stdio: 'inherit' })`ls`).stdout
419 const r2 = $.sync({ stdio: 'inherit' })`ls`.stdout
420 assert.equal(r1, r2)
421 })
422
423 test('mixed', async () => {
424 assert.equal(
425 (
426 await $({
427 quiet: true,
428 stdio: ['inherit', 'pipe', 'ignore'],
429 })`>&2 echo error; echo ok`
430 ).toString(),
431 'ok\n'
432 )
433 })
434
435 test('via stdio() method', async () => {
436 assert.equal(
437 (
438 await $({ halt: true })`>&2 echo error; echo ok`
439 .stdio('inherit', 'ignore', 'pipe')
440 .quiet()
441 .run()
442 ).toString(),
443 'error\n'
444 )
445
446 assert.equal(
447 (
448 await $({ halt: true })`>&2 echo error; echo ok`
449 .stdio(['inherit', 'pipe', 'ignore'])
450 .quiet()
451 .run()
452 ).toString(),
453 'ok\n'
454 )
455 })
456
457 test('file stream as stdout', async () => {
458 const createWriteStream = (f) => {
459 const stream = fs.createWriteStream(f)
460 return new Promise((resolve) => {
461 stream.on('open', () => resolve(stream))
462 })
463 }
464 const file = tempfile()
465 const stream = await createWriteStream(file)
466 const p = $({ stdio: ['pipe', stream, 'ignore'] })`echo foo`
467
468 await p
469 assert.equal((await fs.readFile(file)).toString(), 'foo\n')
470 })
471 })
472
473 it('uses custom `log` if specified', async () => {
474 const entries = []
475 const log = (entry) => entries.push(entry)
476 const p = $({ log })`echo foo`
477 const { id } = p
478 const { duration } = await p
479 const cwd = process.cwd()
480
481 assert.equal(entries.length, 3)
482 assert.deepEqual(entries[0], {
483 kind: 'cmd',
484 cmd: 'echo foo',
485 cwd,
486 verbose: false,
487 id,
488 })
489 assert.deepEqual(entries[1], {
490 kind: 'stdout',
491 data: Buffer.from('foo\n'),
492 verbose: false,
493 id,
494 })
495 assert.deepEqual(entries[2], {
496 kind: 'end',
497 duration,
498 exitCode: 0,
499 signal: null,
500 error: null,
501 verbose: false,
502 id,
503 })
504 })
505 })
506
507 describe('ProcessPromise', () => {
508 test('getters', async () => {
509 const p = $`echo foo`
510 assert.ok(typeof p.pid === 'number')
511 assert.ok(typeof p.id === 'string')
512 assert.ok(typeof p.cmd === 'string')
513 assert.ok(typeof p.fullCmd === 'string')
514 assert.ok(typeof p.stage === 'string')
515 assert.ok(p.child instanceof ChildProcess)
516 assert.ok(p.stdout instanceof Socket)
517 assert.ok(p.stderr instanceof Socket)
518 assert.ok(p.exitCode instanceof Promise)
519 assert.ok(p.signal instanceof AbortSignal)
520 assert.equal(p.output, null)
521 assert.equal(Object.prototype.toString.call(p), '[object ProcessPromise]')
522 assert.equal('' + p, '[object ProcessPromise]')
523 assert.equal(`${p}`, '[object ProcessPromise]')
524 assert.equal(+p, NaN)
525
526 await p
527 assert.ok(p.output instanceof ProcessOutput)
528 })
529
530 test('id is unique', async () => {
531 const p1 = $`echo foo`
532 const p2 = $`echo bar`
533
534 assert.ok(p1.id !== p2.id)
535 assert.ok(p1.id.length > 5)
536 assert.ok(p2.id.length > 5)
537
538 await p1
539 await p2
540 })
541
542 describe('state machine transitions', () => {
543 it('running > fulfilled', async () => {
544 const p = $`echo foo`
545 assert.equal(p.stage, 'running')
546 await p
547 assert.equal(p.stage, 'fulfilled')
548 })
549
550 it('running > rejected', async () => {
551 const p = $`foo`
552 assert.equal(p.stage, 'running')
553
554 try {
555 await p
556 } catch {}
557 assert.equal(p.stage, 'rejected')
558 })
559
560 it('halted > running > fulfilled', async () => {
561 const p = $({ halt: true })`echo foo`
562 assert.equal(p.stage, 'halted')
563 p.run()
564 assert.equal(p.stage, 'running')
565 await p
566 assert.equal(p.stage, 'fulfilled')
567 })
568
569 it('all transitions', async () => {
570 const { promise, resolve, reject } = Promise.withResolvers()
571 const p = new ProcessPromise(noop)
572 ProcessPromise.disarm(p, false)
573 assert.equal(p.stage, 'initial')
574
575 p._resolve = resolve
576 p._reject = reject
577 p._stage = 'halted'
578 p._snapshot = {
579 ...defaults,
580 ac: new AbortController(),
581 from: 'test',
582 cmd: 'echo foo',
583 ee: new EventEmitter(),
584 }
585
586 assert.equal(p.stage, 'halted')
587 p.run()
588 assert.equal(p.stage, 'running')
589 await promise
590 assert.equal(p.stage, 'fulfilled')
591 assert.equal(p.output.stdout, 'foo\n')
592 })
593 })
594
595 test('inherits native Promise', async () => {
596 const p1 = $`echo 1`
597 const p2 = p1.then((v) => v)
598 const p3 = p2.then((v) => v)
599 const p4 = p3.catch((v) => v)
600 const p5 = p1.finally((v) => v)
601
602 assert(p1 instanceof Promise)
603 assert(p1 instanceof ProcessPromise)
604 assert(p2 instanceof ProcessPromise)
605 assert(p3 instanceof ProcessPromise)
606 assert(p4 instanceof ProcessPromise)
607 assert(p5 instanceof ProcessPromise)
608 assert.ok(p1 !== p2)
609 assert.ok(p2 !== p3)
610 assert.ok(p3 !== p4)
611 assert.ok(p5 !== p1)
612 })
613
614 test('asserts self instantiation', async () => {
615 const p = new ProcessPromise(() => {})
616
617 assert(typeof p.then === 'function')
618 assert.throws(() => p.stage, /Inappropriate usage/)
619 })
620
621 test('resolves with ProcessOutput', async () => {
622 const o = await $`echo foo`
623 assert.ok(o instanceof ProcessOutput)
624 })
625
626 test('cmd() returns cmd to exec', () => {
627 const foo = '#bar'
628 const baz = 1
629 const p = $`echo ${foo} --t ${baz}`
630 assert.equal(p.cmd, "echo $'#bar' --t 1")
631 assert.equal(p.fullCmd, "set -euo pipefail;echo $'#bar' --t 1")
632 })
633
634 test('stdin works', async () => {
635 const p = $`read; printf $REPLY`
636 p.stdin.write('bar\n')
637 assert.equal((await p).stdout, 'bar')
638 })
639
640 describe('pipe()', () => {
641 test('accepts Writable', async () => {
642 let contents = ''
643 const stream = new Writable({
644 write: function (chunk, encoding, next) {
645 contents += chunk.toString()
646 next()
647 },
648 })
649 const p1 = $`echo 'test'`
650 const p2 = p1.pipe(stream)
651 assert.equal(p1._piped, true)
652 await p2
653 assert.equal(p1._piped, false)
654 assert.ok(p1.stderr instanceof Socket)
655 assert.equal(contents, 'test\n')
656 })
657
658 test('throws if Writable ended', async () => {
659 const stream = { writableEnded: true }
660 const p = $`echo foo`
661 assert.throws(() => p.pipe(stream), /Cannot pipe to a closed stream/)
662 await p
663 })
664
665 test('accepts WriteStream', async () => {
666 const file = tempfile()
667 try {
668 await $`echo foo`.pipe(fs.createWriteStream(file))
669 assert.equal((await fs.readFile(file)).toString(), 'foo\n')
670
671 const r = $`cat`
672 fs.createReadStream(file).pipe(r.stdin)
673 assert.equal((await r).stdout, 'foo\n')
674 } finally {
675 await fs.rm(file)
676 }
677 })
678
679 test('accepts file', async () => {
680 const file = tempfile()
681 try {
682 await $`echo foo`.pipe(file)
683 assert.equal((await fs.readFile(file)).toString(), 'foo\n')
684
685 const r = $`cat`
686 fs.createReadStream(file).pipe(r.stdin)
687 assert.equal((await r).stdout, 'foo\n')
688 } finally {
689 await fs.rm(file)
690 }
691 })
692
693 test('accepts ProcessPromise', async () => {
694 const p = await $`echo foo`.pipe($`cat`)
695 assert.equal(p.stdout.trim(), 'foo')
696 })
697
698 test('throws if dest ProcessPromise is settled', async () => {
699 const dest = $`echo bar`
700 await dest
701 const p = $`echo foo`
702 assert.throws(() => p.pipe(dest), /Cannot pipe to a settled process/)
703 await p
704 })
705
706 test('detects inappropriate ProcessPromise', async () => {
707 const foo = $`echo foo`
708 const p1 = $`cat`
709 const p2 = p1.then((v) => v)
710
711 assert.throws(() => foo.pipe(p2), /Inappropriate usage/)
712 await foo.pipe(p1)
713 })
714
715 test('accepts $ template literal', async () => {
716 const p = await $`echo foo`.pipe`cat`
717 assert.equal(p.stdout.trim(), 'foo')
718 })
719
720 test('accepts stdout', async () => {
721 const p1 = $`echo pipe-to-stdout`
722 const p2 = p1.pipe(process.stdout)
723 assert.equal((await p1).stdout.trim(), 'pipe-to-stdout')
724 })
725
726 describe('supports chaining', () => {
727 const getUpperCaseTransform = () =>
728 new Transform({
729 transform(chunk, encoding, callback) {
730 callback(null, String(chunk).toUpperCase())
731 },
732 })
733
734 test('$ > $', async () => {
735 const { stdout: o1 } = await $`echo "hello"`
736 .pipe($`awk '{print $1" world"}'`)
737 .pipe($`tr '[a-z]' '[A-Z]'`)
738 assert.equal(o1, 'HELLO WORLD\n')
739
740 const { stdout: o2 } = await $`echo "hello"`
741 .pipe`awk '{print $1" world"}'`.pipe`tr '[a-z]' '[A-Z]'`
742 assert.equal(o2, 'HELLO WORLD\n')
743 })
744
745 test('$ > $ halted', async () => {
746 const $h = $({ halt: true })
747 const { stdout } = await $`echo "hello"`
748 .pipe($h`awk '{print $1" world"}'`)
749 .pipe($h`tr '[a-z]' '[A-Z]'`)
750
751 assert.equal(stdout, 'HELLO WORLD\n')
752 })
753
754 test('$ halted > $ halted', async () => {
755 const $h = $({ halt: true })
756 const { stdout } = await $h`echo "hello"`
757 .pipe($h`awk '{print $1" world"}'`)
758 .pipe($h`tr '[a-z]' '[A-Z]'`)
759 .run()
760
761 assert.equal(stdout, 'HELLO WORLD\n')
762 })
763
764 test('$ halted > $ literal', async () => {
765 const { stdout } = await $({ halt: true })`echo "hello"`
766 .pipe`awk '{print $1" world"}'`.pipe`tr '[a-z]' '[A-Z]'`.run()
767
768 assert.equal(stdout, 'HELLO WORLD\n')
769 })
770
771 test('several $ halted > $ halted', async () => {
772 const $h = $({ halt: true })
773 const p1 = $`echo foo`
774 const p2 = $h`echo a && sleep 0.1 && echo c && sleep 0.2 && echo e`
775 const p3 = $h`sleep 0.05 && echo b && sleep 0.1 && echo d`
776 const p4 = $`sleep 0.4 && echo bar`
777 const p5 = $h`cat`
778
779 await p1
780 p1.pipe(p5)
781 p2.pipe(p5)
782 p3.pipe(p5)
783 p4.pipe(p5)
784
785 const { stdout } = await p5.run()
786
787 assert.equal(stdout, 'foo\na\nb\nc\nd\ne\nbar\n')
788 })
789
790 test('$ > stream', async () => {
791 const file = tempfile()
792 const fileStream = fs.createWriteStream(file)
793 const p = $`echo "hello"`
794 .pipe(getUpperCaseTransform())
795 .pipe(fileStream)
796 const o = await p
797
798 assert.ok(p instanceof WriteStream)
799 assert.ok(o instanceof WriteStream)
800 assert.equal(o.stdout, 'hello\n')
801 assert.equal(o.exitCode, 0)
802 assert.equal((await fs.readFile(file)).toString(), 'HELLO\n')
803 await fs.rm(file)
804 })
805
806 test('$ > stdout', async () => {
807 const p = $`echo 1`.pipe(process.stdout)
808 assert.deepEqual(p, process.stdout)
809 })
810
811 test('$ halted > stream', async () => {
812 const file = tempfile()
813 const fileStream = fs.createWriteStream(file)
814 const p1 = $({ halt: true })`echo "hello"`
815 const p2 = p1.pipe(getUpperCaseTransform()).pipe(fileStream)
816
817 assert.ok(p2 instanceof WriteStream)
818 assert.equal(p2.run(), undefined)
819
820 await p2
821
822 assert.equal((await fs.readFile(file)).toString(), 'HELLO\n')
823 await fs.rm(file)
824 })
825
826 test('stream > $', async () => {
827 const file = tempfile()
828 await fs.writeFile(file, 'test')
829 const { stdout } = await fs
830 .createReadStream(file)
831 .pipe(getUpperCaseTransform())
832 .pipe($`cat`)
833
834 assert.equal(stdout, 'TEST')
835 })
836
837 test('fetch (stream) > $', async () => {
838 // stream.Readable.fromWeb requires Node.js 18+
839 const responseToReadable = (response) => {
840 const reader = response.body.getReader()
841 const rs = new Readable()
842 rs._read = async () => {
843 const result = await reader.read()
844 if (!result.done) rs.push(Buffer.from(result.value))
845 else rs.push(null)
846 }
847 return rs
848 }
849
850 const p = (
851 await fetch('https://example.com').then(responseToReadable)
852 ).pipe($`cat`)
853 const o = await p
854
855 assert.match(o.stdout, /Example Domain/)
856 })
857
858 test('fetch (pipe) > $', async () => {
859 const p1 = fetch('https://example.com').pipe($`cat`)
860 const p2 = fetch('https://example.com').pipe`cat`
861 const o1 = await p1
862 const o2 = await p2
863
864 assert.match(o1.stdout, /Example Domain/)
865 assert.equal(o1.stdout, o2.stdout)
866 })
867
868 test('$ > stream > $', async () => {
869 const p = $`echo "hello"`
870 const { stdout } = await p.pipe(getUpperCaseTransform()).pipe($`cat`)
871
872 assert.equal(stdout, 'HELLO\n')
873 })
874 })
875
876 it('supports delayed piping', async () => {
877 const result = $`echo 1; sleep 1; echo 2; sleep 1; echo 3`
878 const piped1 = result.pipe`cat`
879 let piped2
880
881 setTimeout(() => {
882 piped2 = result.pipe`cat`
883 }, 1500)
884
885 await piped1
886 assert.equal((await piped1).toString(), '1\n2\n3\n')
887 assert.equal((await piped2).toString(), '1\n2\n3\n')
888 })
889
890 it('fulfilled piping', async () => {
891 const p1 = $`echo foo && sleep 0.1 && echo bar`
892 await p1
893 const p2 = p1.pipe`cat`
894 await p2
895
896 assert.equal(p1.output.toString(), 'foo\nbar\n')
897 assert.equal(p2.output.toString(), 'foo\nbar\n')
898 })
899
900 it('rejected piping', async () => {
901 const p1 = $({ nothrow: true })`echo foo && exit 1`
902 await p1
903 const p2 = p1.pipe($({ nothrow: true })`cat`)
904 await p2
905
906 assert.equal(p1.output.toString(), 'foo\n')
907 assert.equal(p1.output.ok, false)
908 assert.equal(p1.output.exitCode, 1)
909
910 assert.equal(p2.output.toString(), 'foo\n')
911 assert.equal(p2.output.ok, false)
912 assert.equal(p2.output.exitCode, 1)
913 })
914
915 test('propagates rejection', async () => {
916 const p1 = $`exit 1`
917 const p2 = p1.pipe($`echo hello`)
918
919 try {
920 await p1
921 } catch (e) {
922 assert.equal(e.exitCode, 1)
923 assert.equal(e.stdout, '')
924 }
925
926 try {
927 await p2
928 } catch (e) {
929 assert.equal(e.exitCode, 1)
930 assert.equal(e.ok, false)
931 }
932
933 const p3 = await $({ nothrow: true })`echo hello && exit 1`.pipe($`cat`)
934 assert.equal(p3.exitCode, 0)
935 assert.equal(p3.stdout.trim(), 'hello')
936
937 const p4 = $`exit 1`.pipe($`echo hello`)
938 try {
939 await p4
940 } catch (e) {
941 assert.equal(e.exitCode, 1)
942 assert.equal(e.ok, false)
943 }
944
945 const p5 = $`echo bar && sleep 0.1 && exit 1`
946 const [r1, r2, r3] = await Promise.allSettled([
947 p5.pipe($`cat`),
948 p5.pipe($({ nothrow: true })`cat`),
949 p5.pipe($({ nothrow: true, halt: true })`cat`),
950 ])
951 assert.equal(r1.reason.stdout, 'bar\n')
952 assert.equal(r1.reason.exitCode, 1)
953 assert.equal(r1.reason.ok, false)
954
955 assert.equal(r2.value.stdout, 'bar\n')
956 assert.equal(r2.value.exitCode, 1)
957 assert.equal(r2.value.ok, false)
958
959 assert.equal(r3.value.stdout, 'bar\n')
960 assert.equal(r3.value.exitCode, 1)
961 assert.equal(r3.value.ok, false)
962
963 const p6 = $`echo bar && exit 1`
964 const [r4, r5] = await Promise.allSettled([
965 p6.pipe($`cat`),
966 p6.pipe($({ nothrow: true })`cat`),
967 ])
968 assert.equal(r4.reason.stdout, 'bar\n')
969 assert.equal(r4.reason.exitCode, 1)
970 assert.equal(r4.reason.ok, false)
971
972 assert.equal(r5.value.stdout, 'bar\n')
973 assert.equal(r5.value.exitCode, 1)
974 assert.equal(r5.value.ok, false)
975 })
976
977 test('pipes particular stream: stdout, stderr, stdall', async () => {
978 const p = $`echo foo >&2; sleep 0.01 && echo bar`
979 const o1 = (await p.pipe.stderr`cat`).toString()
980 const o2 = (await p.pipe.stdout`cat`).toString()
981 const o3 = (await p.pipe.stdall`cat`).toString()
982
983 assert.equal(o1, 'foo\n')
984 assert.equal(o2, 'bar\n')
985 assert.equal(o3, 'foo\nbar\n')
986 })
987 })
988
989 describe('unpipe()', () => {
990 it('disables piping', async () => {
991 const p1 = $`echo foo && sleep 0.2 && echo bar && sleep 0.3 && echo baz && sleep 0.4 && echo qux`
992 const p2 = $`echo 1 && sleep 0.3 && echo 2 && sleep 0.2 && echo 3`
993 const p3 = $`cat`
994
995 p1.pipe(p3)
996 p2.pipe(p3)
997
998 setTimeout(() => p1.unpipe(p3), 300)
999
1000 const { stdout } = await p3
1001 assert.equal(stdout, 'foo\n1\nbar\n2\n3\n')
1002 })
1003 })
1004
1005 describe('abort()', () => {
1006 test('just works', async () => {
1007 const p = $({ detached: true })`sleep 999`
1008 setTimeout(() => p.abort(), 100)
1009
1010 try {
1011 await p
1012 assert.unreachable('should have thrown')
1013 } catch ({ message }) {
1014 assert.match(message, /The operation was aborted/)
1015 }
1016 })
1017
1018 test('accepts optional AbortController', async () => {
1019 const ac = new AbortController()
1020 const p = $({ ac, detached: true })`sleep 999`
1021 setTimeout(() => ac.abort(), 100)
1022
1023 try {
1024 await p
1025 assert.unreachable('should have thrown')
1026 } catch ({ message }) {
1027 assert.match(message, /The operation was aborted/)
1028 }
1029 })
1030
1031 test('accepts AbortController `signal` separately', async () => {
1032 const ac = new AbortController()
1033 const signal = ac.signal
1034 const p = $({ signal, detached: true })`sleep 999`
1035 setTimeout(() => ac.abort(), 100)
1036
1037 try {
1038 await p
1039 assert.unreachable('should have thrown')
1040 } catch ({ message }) {
1041 assert.match(message, /The operation was aborted/)
1042 }
1043 })
1044
1045 describe('handles halt option', () => {
1046 test('just works', async () => {
1047 const filepath = `${tempdir()}/${Math.random().toString()}`
1048 const p = $({ halt: true })`touch ${filepath}`
1049 await sleep(1)
1050 assert.ok(
1051 !fs.existsSync(filepath),
1052 'The cmd called, but it should not have been called'
1053 )
1054 await p.run()
1055 assert.ok(fs.existsSync(filepath), 'The cmd should have been called')
1056 })
1057
1058 test('sync process ignores halt option', () => {
1059 const p = $.sync({ halt: true })`echo foo`
1060 assert.equal(p.stdout, 'foo\n')
1061 })
1062 })
1063
1064 test('exposes `signal` property', async () => {
1065 const ac = new AbortController()
1066 const p = $({ ac, detached: true })`echo test`
1067
1068 assert.equal(p.signal, ac.signal)
1069 await p
1070 })
1071
1072 test('throws if the signal was previously aborted', async () => {
1073 const ac = new AbortController()
1074 const { signal } = ac
1075 ac.abort('reason')
1076
1077 try {
1078 await $({ signal, detached: true })`sleep 999`
1079 } catch ({ message }) {
1080 assert.match(message, /The operation was aborted/)
1081 }
1082 })
1083
1084 test('throws if the signal is controlled by another process', async () => {
1085 const ac = new AbortController()
1086 const { signal } = ac
1087 const p = $({ signal })`sleep 999`
1088
1089 try {
1090 p.abort()
1091 } catch ({ message }) {
1092 assert.match(message, /The signal is controlled by another process./)
1093 }
1094
1095 try {
1096 ac.abort()
1097 await p
1098 } catch ({ message }) {
1099 assert.match(message, /The operation was aborted/)
1100 }
1101 })
1102
1103 test('throws if too late', async () => {
1104 const p = $`echo foo`
1105 await p
1106
1107 assert.throws(() => p.abort(), /Too late to abort the process/)
1108 })
1109
1110 test('abort signal is transmittable through pipe', async () => {
1111 const ac = new AbortController()
1112 const { signal } = ac
1113 const p1 = $({ signal, nothrow: true })`echo test`
1114 const p2 = p1.pipe`sleep 999`
1115 setTimeout(() => ac.abort(), 50)
1116
1117 try {
1118 await p2
1119 } catch ({ message }) {
1120 assert.match(message, /The operation was aborted/)
1121 }
1122 })
1123 })
1124
1125 describe('kill()', () => {
1126 test('just works', async () => {
1127 const p = $`sleep 999`.nothrow()
1128 setTimeout(() => {
1129 p.kill()
1130 }, 100)
1131 const o = await p
1132 assert.equal(o.signal, 'SIGTERM')
1133 assert.ok(o.duration >= 100 && o.duration < 1000)
1134 })
1135
1136 test('applies custom signal if passed', async () => {
1137 const p = $`while true; do :; done`
1138 setTimeout(() => p.kill('SIGKILL'), 100)
1139 let signal
1140 try {
1141 await p
1142 } catch (p) {
1143 signal = p.signal
1144 }
1145 assert.equal(signal, 'SIGKILL')
1146 })
1147
1148 test('applies `$.killSignal` if defined', async () => {
1149 const p = $({ killSignal: 'SIGKILL' })`while true; do :; done`
1150 setTimeout(() => p.kill(), 100)
1151 let signal
1152 try {
1153 await p
1154 } catch (p) {
1155 signal = p.signal
1156 }
1157 assert.equal(signal, 'SIGKILL')
1158 })
1159
1160 test('throws if too late', async () => {
1161 const p = $`echo foo`
1162 await p
1163
1164 assert.throws(() => p.kill(), /Too late to kill the process/)
1165 })
1166
1167 test('throws if too early', async () => {
1168 const p = $({ halt: true })`echo foo`
1169
1170 assert.throws(
1171 () => p.kill(),
1172 /Trying to kill a process without creating one/
1173 )
1174 })
1175
1176 test('throws if pid is empty', async () => {
1177 const p = $({
1178 spawn() {
1179 return new EventEmitter()
1180 },
1181 })`echo foo`
1182
1183 assert.throws(() => p.kill(), /The process pid is undefined/)
1184 })
1185 })
1186
1187 describe('[Symbol.asyncIterator]', () => {
1188 it('should iterate over lines from stdout', async () => {
1189 const process = $`echo "Line1\nLine2\nLine3"`
1190 const lines = []
1191 for await (const line of process) {
1192 lines.push(line)
1193 }
1194
1195 assert.deepEqual(lines, ['Line1', 'Line2', 'Line3'])
1196 })
1197
1198 it('should handle partial lines correctly', async () => {
1199 const process = $`node -e "process.stdout.write('PartialLine1\\nLine2\\nPartial'); setTimeout(() => process.stdout.write('Line3\\n'), 100)"`
1200 const lines = []
1201 for await (const line of process) {
1202 lines.push(line)
1203 }
1204
1205 assert.deepEqual(lines, ['PartialLine1', 'Line2', 'PartialLine3'])
1206 })
1207
1208 it('should handle empty stdout', async () => {
1209 const process = $`echo -n ""`
1210 const lines = []
1211 for await (const line of process) {
1212 lines.push(line)
1213 }
1214
1215 assert.equal(lines.length, 0, 'Should have 0 lines for empty stdout')
1216 })
1217
1218 it('should handle single line without trailing newline', async () => {
1219 const process = $`echo -n "SingleLine"`
1220 const lines = []
1221 for await (const line of process) {
1222 lines.push(line)
1223 }
1224
1225 assert.deepEqual(lines, ['SingleLine'])
1226 })
1227
1228 it('should yield all buffered and new chunks when iterated after a delay', async () => {
1229 const process = $`sleep 0.1; echo Chunk1; sleep 0.1; echo Chunk2; sleep 0.2; echo Chunk3; sleep 0.1; echo Chunk4;`
1230 const chunks = []
1231
1232 await sleep(250)
1233 for await (const chunk of process) {
1234 chunks.push(chunk)
1235 }
1236
1237 assert.equal(chunks.length, 4, 'Should get all chunks')
1238 assert.equal(chunks[0], 'Chunk1', 'First chunk should be "Chunk1"')
1239 assert.equal(chunks[3], 'Chunk4', 'Second chunk should be "Chunk4"')
1240 })
1241
1242 it('handles ignored stdio', async () => {
1243 const p = $({
1244 stdio: 'ignore',
1245 })`sleep 0.1; echo Chunk1; sleep 0.1; echo Chunk2`
1246 const chunks = []
1247 for await (const chunk of p) {
1248 chunks.push(chunk)
1249 }
1250
1251 assert.equal(chunks.length, 0)
1252 assert.equal((await p).stdout, '')
1253 })
1254
1255 it('handles non-iterable stdio', async () => {
1256 const file = tempfile()
1257 const fd = fs.openSync(file, 'w')
1258 const p = $({
1259 stdio: ['ignore', fd, 'ignore'],
1260 })`sleep 0.1; echo Chunk1; sleep 0.1; echo Chunk2`
1261 const chunks = []
1262 for await (const chunk of p) {
1263 chunks.push(chunk)
1264 }
1265
1266 assert.equal(chunks.length, 0)
1267 assert.equal((await p).stdout, '')
1268 assert.equal(fs.readFileSync(file, 'utf-8'), `Chunk1\nChunk2\n`)
1269 })
1270
1271 it('should process all output before handling a non-zero exit code', async () => {
1272 const process = $`sleep 0.1; echo foo; sleep 0.1; echo bar; sleep 0.1; exit 1;`
1273 const chunks = []
1274
1275 let errorCaught = null
1276 try {
1277 for await (const chunk of process) {
1278 chunks.push(chunk)
1279 }
1280 } catch (err) {
1281 errorCaught = err
1282 }
1283
1284 assert.equal(chunks.length, 2, 'Should have received 2 chunks')
1285 assert.equal(chunks[0], 'foo', 'First chunk should be "foo"')
1286 assert.equal(chunks[1], 'bar', 'Second chunk should be "bar"')
1287
1288 assert.ok(errorCaught, 'An error should have been caught')
1289 assert.equal(
1290 errorCaught.exitCode,
1291 1,
1292 'The process exit code should be 1'
1293 )
1294 })
1295
1296 it('handles .nothrow() correctly', async () => {
1297 const lines = []
1298 for await (const line of $({ nothrow: true })`grep any test`) {
1299 lines.push(line)
1300 }
1301 assert.equal(lines.length, 0, 'Should not yield any lines')
1302 })
1303
1304 it('handles a custom delimiter', async () => {
1305 const lines = []
1306 for await (const line of $({
1307 delimiter: '\0',
1308 cwd: tempdir(),
1309 })`touch foo bar baz; find ./ -type f -print0 -maxdepth 1`) {
1310 lines.push(line)
1311 }
1312 assert.deepEqual(lines.sort(), ['./bar', './baz', './foo'])
1313 })
1314 })
1315
1316 test('quiet() mode is working', async () => {
1317 const log = console.log
1318 let stdout = ''
1319 console.log = (...args) => {
1320 stdout += args.join(' ')
1321 }
1322 await $`echo 'test'`.quiet()
1323 console.log = log
1324 assert.equal(stdout, '')
1325 {
1326 // Deprecated.
1327 let stdout = ''
1328 console.log = (...args) => {
1329 stdout += args.join(' ')
1330 }
1331 await quiet($`echo 'test'`)
1332 console.log = log
1333 assert.equal(stdout, '')
1334 }
1335 })
1336
1337 test('verbose() mode is working', async () => {
1338 const p = $`echo 'test'`
1339 assert.equal(p.isVerbose(), false)
1340
1341 p.verbose()
1342 assert.equal(p.isVerbose(), true)
1343
1344 p.verbose(false)
1345 assert.equal(p.isVerbose(), false)
1346 })
1347
1348 test('nothrow() does not throw', async () => {
1349 {
1350 const { exitCode } = await $`exit 42`.nothrow()
1351 assert.equal(exitCode, 42)
1352 }
1353 {
1354 // Toggle
1355 try {
1356 const p = $`exit 42`.nothrow()
1357 await p.nothrow(false)
1358 } catch ({ exitCode }) {
1359 assert.equal(exitCode, 42)
1360 }
1361 }
1362 {
1363 // Deprecated.
1364 const { exitCode } = await nothrow($`exit 42`)
1365 assert.equal(exitCode, 42)
1366 }
1367 })
1368
1369 describe('timeout()', () => {
1370 test('expiration works', async () => {
1371 await $`sleep 1`.timeout(1000)
1372 let exitCode, signal
1373 try {
1374 await $`sleep 1`.timeout(200)
1375 } catch (p) {
1376 exitCode = p.exitCode
1377 signal = p.signal
1378 }
1379 assert.equal(exitCode, null)
1380 assert.equal(signal, 'SIGTERM')
1381 })
1382
1383 test('accepts a signal opt', async () => {
1384 let exitCode, signal
1385 try {
1386 await $`sleep 999`.timeout(10, 'SIGKILL')
1387 } catch (p) {
1388 exitCode = p.exitCode
1389 signal = p.signal
1390 }
1391 assert.equal(exitCode, null)
1392 assert.equal(signal, 'SIGKILL')
1393 })
1394 })
1395
1396 test('json()', async () => {
1397 assert.deepEqual(await $`echo '{"key":"value"}'`.json(), { key: 'value' })
1398 })
1399
1400 test('text()', async () => {
1401 const p = $`echo foo`
1402 assert.equal(await p.text(), 'foo\n')
1403 assert.equal(await p.text('hex'), '666f6f0a')
1404 })
1405
1406 test('lines()', async () => {
1407 const p1 = $`echo 'foo\nbar\r\nbaz'`
1408 assert.deepEqual(await p1.lines(), ['foo', 'bar', 'baz'])
1409
1410 const p2 = $.sync`echo 'foo\nbar\r\nbaz'`
1411 assert.deepEqual(p2.lines(), ['foo', 'bar', 'baz'])
1412
1413 const p3 = $({
1414 cwd: await tempdir(),
1415 })`touch foo bar baz; find ./ -type f -print0 -maxdepth 1`
1416 assert.deepEqual((await p3.lines('\0')).sort(), [
1417 './bar',
1418 './baz',
1419 './foo',
1420 ])
1421 })
1422
1423 test('buffer()', async () => {
1424 assert.equal(
1425 (await $`echo foo`.buffer()).compare(Buffer.from('foo\n', 'utf-8')),
1426 0
1427 )
1428 })
1429
1430 test('blob()', async () => {
1431 const p = $`echo foo`
1432 assert.equal(await (await p.blob()).text(), 'foo\n')
1433 })
1434 })
1435
1436 describe('ProcessOutput', () => {
1437 test('getters', async () => {
1438 const o = new ProcessOutput(-1, 'SIGTERM', '', '', 'foo\n', 'msg', 20)
1439
1440 assert.equal(o.stdout, '')
1441 assert.equal(o.stderr, '')
1442 assert.equal(o.stdall, 'foo\n')
1443 assert.equal(o.signal, 'SIGTERM')
1444 assert.equal(o.exitCode, -1)
1445 assert.equal(o.duration, 20)
1446 assert.equal(o.ok, false)
1447 assert.equal(
1448 o.message,
1449 'msg\n errno: undefined (Unknown error)\n code: undefined\n at '
1450 )
1451 assert.equal(Object.prototype.toString.call(o), '[object ProcessOutput]')
1452
1453 const o1 = new ProcessOutput({
1454 code: -1,
1455 from: 'file.js(12:34)',
1456 store: {
1457 stdall: ['error in stdout'],
1458 stdout: [],
1459 stderr: [],
1460 },
1461 })
1462 assert.equal(
1463 o1.message,
1464 '\n at file.js(12:34)\n exit code: -1\n details: \nerror in stdout'
1465 )
1466 })
1467
1468 test('[Symbol.toPrimitive]', () => {
1469 const o = new ProcessOutput(-1, 'SIGTERM', '', '', 'foo\n', 'msg', 20)
1470 assert.equal('' + o, 'foo')
1471 assert.equal(`${o}`, 'foo')
1472 assert.equal(+o, NaN)
1473 })
1474
1475 test('toString()', async () => {
1476 const o = new ProcessOutput(null, null, '', '', 'foo\n')
1477 assert.equal(o.toString(), 'foo\n')
1478 })
1479
1480 test('valueOf()', async () => {
1481 const o = new ProcessOutput(null, null, '', '', 'foo\n')
1482 assert.equal(o.valueOf(), 'foo')
1483 assert.ok(o == 'foo')
1484 })
1485
1486 test('json()', async () => {
1487 const o = new ProcessOutput(null, null, '', '', '{"key":"value"}')
1488 assert.deepEqual(o.json(), { key: 'value' })
1489 })
1490
1491 test('text()', async () => {
1492 const o = new ProcessOutput(null, null, '', '', 'foo\n')
1493 assert.equal(o.text(), 'foo\n')
1494 assert.equal(o.text('hex'), '666f6f0a')
1495 })
1496
1497 test('lines()', async () => {
1498 const o1 = new ProcessOutput(null, null, '', '', 'foo\nbar\r\nbaz\n')
1499 assert.deepEqual(o1.lines(), ['foo', 'bar', 'baz'])
1500
1501 const o2 = new ProcessOutput(null, null, '', '', 'foo\0bar\0baz\0')
1502 assert.deepEqual(o2.lines(), ['foo\0bar\0baz\0'])
1503 assert.deepEqual(o2.lines('\0'), ['foo', 'bar', 'baz'])
1504 })
1505
1506 test('buffer()', async () => {
1507 const o = new ProcessOutput(null, null, '', '', 'foo\n')
1508 assert.equal(o.buffer().compare(Buffer.from('foo\n', 'utf-8')), 0)
1509 })
1510
1511 test('blob()', async () => {
1512 const o = new ProcessOutput(null, null, '', '', 'foo\n')
1513 assert.equal(await o.blob().text(), 'foo\n')
1514
1515 const { Blob } = globalThis
1516 globalThis.Blob = undefined
1517 assert.throws(() => o.blob(), /Blob is not supported/)
1518 globalThis.Blob = Blob
1519 })
1520
1521 test('[Symbol.Iterator]', () => {
1522 const o = new ProcessOutput({
1523 store: {
1524 stdall: ['foo\nba', 'r\nbaz'],
1525 },
1526 })
1527 const lines = []
1528 const expected = ['foo', 'bar', 'baz']
1529 for (const line of o) {
1530 lines.push(line)
1531 }
1532 assert.deepEqual(lines, expected)
1533 assert.deepEqual(o.lines(), expected)
1534 assert.deepEqual([...o], expected) // isConcatSpreadable
1535 })
1536
1537 describe('static', () => {
1538 test('getExitMessage()', () => {
1539 assert.match(
1540 ProcessOutput.getExitMessage(2, null, '', ''),
1541 /Misuse of shell builtins/
1542 )
1543 })
1544
1545 test('getErrorMessage()', () => {
1546 assert.match(
1547 ProcessOutput.getErrorMessage({ errno: -2 }, ''),
1548 /No such file or directory/
1549 )
1550 assert.match(
1551 ProcessOutput.getErrorMessage({ errno: -1e9 }, ''),
1552 /Unknown error/
1553 )
1554 assert.match(ProcessOutput.getErrorMessage({}, ''), /Unknown error/)
1555 })
1556 })
1557 })
1558
1559 describe('cd()', () => {
1560 test('works with relative paths', async () => {
1561 const cwd = process.cwd()
1562 try {
1563 fs.mkdirpSync('/tmp/zx-cd-test/one/two')
1564 cd('/tmp/zx-cd-test/one/two')
1565 const p1 = $`pwd`
1566 assert.equal($.cwd, undefined)
1567 assert.ok(process.cwd().endsWith('/two'))
1568
1569 cd('..')
1570 const p2 = $`pwd`
1571 assert.equal($.cwd, undefined)
1572 assert.ok(process.cwd().endsWith('/one'))
1573
1574 cd('..')
1575 const p3 = $`pwd`
1576 assert.equal($.cwd, undefined)
1577 assert.ok(process.cwd().endsWith('/tmp/zx-cd-test'))
1578
1579 const results = (await Promise.all([p1, p2, p3])).map((p) =>
1580 basename(p.stdout.trim())
1581 )
1582 assert.deepEqual(results, ['two', 'one', 'zx-cd-test'])
1583 } catch (e) {
1584 assert.ok(!e, e)
1585 } finally {
1586 fs.rmSync('/tmp/zx-cd-test', { recursive: true })
1587 cd(cwd)
1588 }
1589 })
1590
1591 test('does not affect parallel contexts ($.cwdSyncHook enabled)', async () => {
1592 syncProcessCwd()
1593 const cwd = process.cwd()
1594 try {
1595 fs.mkdirpSync('/tmp/zx-cd-parallel/one/two')
1596 await Promise.all([
1597 within(async () => {
1598 assert.equal(process.cwd(), cwd)
1599 cd('/tmp/zx-cd-parallel/one')
1600 await sleep(Math.random() * 15)
1601 assert.ok(process.cwd().endsWith('/tmp/zx-cd-parallel/one'))
1602 }),
1603 within(async () => {
1604 assert.equal(process.cwd(), cwd)
1605 await sleep(Math.random() * 15)
1606 assert.equal(process.cwd(), cwd)
1607 }),
1608 within(async () => {
1609 assert.equal(process.cwd(), cwd)
1610 await sleep(Math.random() * 15)
1611 $.cwd = '/tmp/zx-cd-parallel/one/two'
1612 assert.equal(process.cwd(), cwd)
1613 assert.ok(
1614 (await $`pwd`).stdout
1615 .toString()
1616 .trim()
1617 .endsWith('/tmp/zx-cd-parallel/one/two')
1618 )
1619 }),
1620 ])
1621 } catch (e) {
1622 assert.ok(!e, e)
1623 } finally {
1624 fs.rmSync('/tmp/zx-cd-parallel', { recursive: true })
1625 cd(cwd)
1626 syncProcessCwd(false)
1627 }
1628 })
1629
1630 test('fails on entering not existing dir', async () => {
1631 assert.throws(() => cd('/tmp/abra-kadabra'))
1632 })
1633
1634 test('accepts ProcessOutput in addition to string', async () => {
1635 await within(async () => {
1636 const tmp = await $`mktemp -d`
1637 cd(tmp)
1638 assert.equal(
1639 basename(process.cwd()),
1640 basename(tmp.toString().trimEnd())
1641 )
1642 })
1643 })
1644 })
1645
1646 describe('kill()', () => {
1647 test('throws if pid is invalid', async () => {
1648 await assert.rejects(() => kill(''), /Invalid/)
1649 await assert.rejects(() => kill('foo'), /Invalid/)
1650 await assert.rejects(() => kill('100 foo'), /Invalid/)
1651 await assert.rejects(() => kill(100.1), /Invalid/)
1652 await assert.rejects(() => kill(null), /Invalid/)
1653 await assert.rejects(() => kill({}), /Invalid/)
1654 await assert.rejects(
1655 () =>
1656 kill({
1657 toString() {
1658 return '12345'
1659 },
1660 }),
1661 /Invalid/
1662 )
1663 })
1664 })
1665
1666 describe('within()', () => {
1667 test('just works', async () => {
1668 let resolve, reject
1669 const promise = new Promise((...args) => ([resolve, reject] = args))
1670
1671 function yes() {
1672 assert.equal($.verbose, true)
1673 resolve()
1674 }
1675
1676 assert.equal($.verbose, false)
1677
1678 within(() => {
1679 $.verbose = true
1680 })
1681 assert.equal($.verbose, false)
1682
1683 within(async () => {
1684 $.verbose = true
1685 setTimeout(yes, 10)
1686 })
1687 assert.equal($.verbose, false)
1688
1689 await promise
1690 })
1691
1692 test('keeps the cwd ref for internal $ calls', async () => {
1693 let resolve, reject
1694 const promise = new Promise((...args) => ([resolve, reject] = args))
1695 const cwd = process.cwd()
1696 const pwd = await $`pwd`
1697
1698 within(async () => {
1699 cd('/tmp')
1700 assert.ok(process.cwd().endsWith('/tmp'))
1701 assert.ok((await $`pwd`).stdout.trim().endsWith('/tmp'))
1702
1703 setTimeout(async () => {
1704 process.chdir('/')
1705 assert.ok((await $`pwd`).stdout.trim().endsWith('/tmp'))
1706 resolve()
1707 process.chdir(cwd)
1708 }, 1000)
1709 })
1710
1711 assert.equal((await $`pwd`).stdout, pwd.stdout)
1712 await promise
1713 })
1714
1715 test(`isolates nested context and returns cb result`, async () => {
1716 within(async () => {
1717 const res = await within(async () => {
1718 $.verbose = true
1719
1720 return within(async () => {
1721 assert.equal($.verbose, true)
1722 $.verbose = false
1723
1724 return within(async () => {
1725 assert.equal($.verbose, false)
1726 $.verbose = true
1727 return 'foo'
1728 })
1729 })
1730 })
1731 assert.equal($.verbose, false)
1732 assert.equal(res, 'foo')
1733 })
1734 })
1735 })
1736
1737 describe('shell presets', () => {
1738 const originalWhichSync = which.sync
1739 before(() => {
1740 which.sync = (bin) => bin
1741 })
1742 after(() => {
1743 which.sync = originalWhichSync
1744 useBash()
1745 })
1746
1747 test('usePwsh()', () => {
1748 usePwsh()
1749 assert.equal($.shell, 'pwsh')
1750 assert.equal($.prefix, '')
1751 assert.equal($.postfix, '; exit $LastExitCode')
1752 assert.equal($.quote, quotePowerShell)
1753 })
1754
1755 test('usePowerShell()', () => {
1756 usePowerShell()
1757 assert.equal($.shell, 'powershell.exe')
1758 assert.equal($.prefix, '')
1759 assert.equal($.postfix, '; exit $LastExitCode')
1760 assert.equal($.quote, quotePowerShell)
1761 })
1762
1763 test('useBash()', () => {
1764 useBash()
1765 assert.equal($.shell, 'bash')
1766 assert.equal($.prefix, 'set -euo pipefail;')
1767 assert.equal($.postfix, '')
1768 assert.equal($.quote, quote)
1769 })
1770 })
1771})