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})