From d09edf89aa3a6bf66a6e780b0ca6decb812bc703 Mon Sep 17 00:00:00 2001 From: Anton Golub Date: Mon, 20 May 2024 16:39:57 +0300 Subject: [PATCH] test: refactor core tests, add scopes (#814) --- test/core.test.js | 1056 +++++++++++++++++++++++---------------------- 1 file changed, 545 insertions(+), 511 deletions(-) diff --git a/test/core.test.js b/test/core.test.js index c9be502b09..00779dafe5 100644 --- a/test/core.test.js +++ b/test/core.test.js @@ -22,599 +22,633 @@ import { ProcessPromise, ProcessOutput } from '../build/index.js' import '../build/globals.js' describe('core', () => { - test('only stdout is used during command substitution', async () => { - let hello = await $`echo Error >&2; echo Hello` - let len = +(await $`echo ${hello} | wc -c`) - assert.equal(len, 6) - }) + describe('$', () => { + test('is a regular function', async () => { + const _$ = $.bind(null) + let foo = await _$`echo foo` + assert.equal(foo.stdout, 'foo\n') + assert.ok(typeof $.call === 'function') + assert.ok(typeof $.apply === 'function') + }) - test('env vars works', async () => { - process.env.ZX_TEST_FOO = 'foo' - let foo = await $`echo $ZX_TEST_FOO` - assert.equal(foo.stdout, 'foo\n') - }) + test('only stdout is used during command substitution', async () => { + let hello = await $`echo Error >&2; echo Hello` + let len = +(await $`echo ${hello} | wc -c`) + assert.equal(len, 6) + }) - test('env vars is safe to pass', async () => { - process.env.ZX_TEST_BAR = 'hi; exit 1' - await $`echo $ZX_TEST_BAR` - }) + test('env vars works', async () => { + process.env.ZX_TEST_FOO = 'foo' + let foo = await $`echo $ZX_TEST_FOO` + assert.equal(foo.stdout, 'foo\n') + }) - test('arguments are quoted', async () => { - let bar = 'bar"";baz!$#^$\'&*~*%)({}||\\/' - assert.equal((await $`echo ${bar}`).stdout.trim(), bar) - }) + test('env vars is safe to pass', async () => { + process.env.ZX_TEST_BAR = 'hi; exit 1' + await $`echo $ZX_TEST_BAR` + }) - test('undefined and empty string correctly quoted', async () => { - assert.equal((await $`echo -n ${undefined}`).toString(), 'undefined') - assert.equal((await $`echo -n ${''}`).toString(), '') - }) + test('arguments are quoted', async () => { + let bar = 'bar"";baz!$#^$\'&*~*%)({}||\\/' + assert.equal((await $`echo ${bar}`).stdout.trim(), bar) + }) + + test('undefined and empty string correctly quoted', async () => { + assert.equal((await $`echo -n ${undefined}`).toString(), 'undefined') + assert.equal((await $`echo -n ${''}`).toString(), '') + }) - test.skip('handles multiline literals', async () => { - assert.equal( - ( - await $`echo foo + test.skip('handles multiline literals', async () => { + assert.equal( + ( + await $`echo foo bar "baz qux" ` - ).toString(), - 'foo bar baz\n qux\n' - ) - assert.equal( - ( - await $`echo foo \ + ).toString(), + 'foo bar baz\n qux\n' + ) + assert.equal( + ( + await $`echo foo \ bar \ baz \ ` - ).toString(), - 'foo bar baz\n' - ) - }) - - test('can create a dir with a space in the name', async () => { - let name = 'foo bar' - try { - await $`mkdir /tmp/${name}` - } catch { - assert.unreachable() - } finally { - await fs.rmdir('/tmp/' + name) - } - }) - - test('pipefail is on', async () => { - let p - try { - p = await $`cat /dev/not_found | sort` - } catch (e) { - p = e - } - assert.notEqual(p.exitCode, 0) - }) - - test('toString() is called on arguments', async () => { - let foo = 0 - let p = await $`echo ${foo}` - assert.equal(p.stdout, '0\n') - }) - - test('can use array as an argument', async () => { - let args = ['-n', 'foo'] - assert.equal((await $`echo ${args}`).toString(), 'foo') - }) + ).toString(), + 'foo bar baz\n' + ) + }) - test('quiet() mode is working', async () => { - let stdout = '' - let log = console.log - console.log = (...args) => { - stdout += args.join(' ') - } - await $`echo 'test'`.quiet() - console.log = log - assert.equal(stdout, '') - { - // Deprecated. - let stdout = '' - let log = console.log - console.log = (...args) => { - stdout += args.join(' ') + test('can create a dir with a space in the name', async () => { + let name = 'foo bar' + try { + await $`mkdir /tmp/${name}` + } catch { + assert.unreachable() + } finally { + await fs.rmdir('/tmp/' + name) } - await quiet($`echo 'test'`) - console.log = log - assert.equal(stdout, '') - } - }) + }) - test('handles `input` option', async () => { - const p1 = $({ input: 'foo' })`cat` - const p2 = $({ input: Readable.from('bar') })`cat` - const p3 = $({ input: Buffer.from('baz') })`cat` - const p4 = $({ input: p3 })`cat` - const p5 = $({ input: await p3 })`cat` - - assert.equal((await p1).stdout, 'foo') - assert.equal((await p2).stdout, 'bar') - assert.equal((await p3).stdout, 'baz') - assert.equal((await p4).stdout, 'baz') - assert.equal((await p5).stdout, 'baz') - }) + test('pipefail is on', async () => { + let p + try { + p = await $`cat /dev/not_found | sort` + } catch (e) { + p = e + } + assert.notEqual(p.exitCode, 0) + }) - test('requires $.shell to be specified', async () => { - await within(() => { - $.shell = undefined - assert.throws(() => $`echo foo`, /shell/) + test('toString() is called on arguments', async () => { + let foo = 0 + let p = await $`echo ${foo}` + assert.equal(p.stdout, '0\n') }) - }) - test('`$.sync()` provides synchronous API', () => { - const o1 = $.sync`echo foo` - const o2 = $({ sync: true })`echo foo` - assert.equal(o1.stdout, 'foo\n') - assert.equal(o2.stdout, 'foo\n') - }) + test('can use array as an argument', async () => { + let args = ['-n', 'foo'] + assert.equal((await $`echo ${args}`).toString(), 'foo') + }) - test('pipes are working', async () => { - let { stdout } = await $`echo "hello"` - .pipe($`awk '{print $1" world"}'`) - .pipe($`tr '[a-z]' '[A-Z]'`) - assert.equal(stdout, 'HELLO WORLD\n') - - try { - await $`echo foo`.pipe(fs.createWriteStream('/tmp/output.txt')) - assert.equal((await fs.readFile('/tmp/output.txt')).toString(), 'foo\n') - - let r = $`cat` - fs.createReadStream('/tmp/output.txt').pipe(r.stdin) - assert.equal((await r).stdout, 'foo\n') - } finally { - await fs.rm('/tmp/output.txt') - } - }) + test('requires $.shell to be specified', async () => { + await within(() => { + $.shell = undefined + assert.throws(() => $`echo foo`, /shell/) + }) + }) - test('provides presets', async () => { - const $$ = $({ nothrow: true }) - assert.equal((await $$`exit 1`).exitCode, 1) - }) + test('malformed cmd error', async () => { + assert.throws(() => $`\033`, /malformed/i) + }) - test('ProcessPromise', async () => { - let contents = '' - let stream = new Writable({ - write: function (chunk, encoding, next) { - contents += chunk.toString() - next() - }, - }) - let p = $`echo 'test'`.pipe(stream) - await p - assert.ok(p._piped) - assert.equal(contents, 'test\n') - assert.ok(p.stderr instanceof Socket) - - let err - try { - $`echo 'test'`.pipe('str') - } catch (p) { - err = p - } - assert.equal( - err.message, - 'The pipe() method does not take strings. Forgot $?' - ) - }) + test('snapshots works', async () => { + await within(async () => { + $.prefix += 'echo success;' + let p = $`:` + $.prefix += 'echo fail;' + let out = await p + assert.equal(out.stdout, 'success\n') + assert.doesNotMatch(out.stdout, /fail/) + }) + }) - test('ProcessPromise: inherits native Promise', async () => { - const p1 = $`echo 1` - const p2 = p1.then((v) => v) - const p3 = p2.then((v) => v) - const p4 = p3.catch((v) => v) - const p5 = p1.finally((v) => v) - - assert(p1 instanceof Promise) - assert(p1 instanceof ProcessPromise) - assert(p2 instanceof ProcessPromise) - assert(p3 instanceof ProcessPromise) - assert(p4 instanceof ProcessPromise) - assert(p5 instanceof ProcessPromise) - assert.ok(p1 !== p2) - assert.ok(p2 !== p3) - assert.ok(p3 !== p4) - assert.ok(p5 !== p1) - }) + test('$ thrown as error', async () => { + let err + try { + await $`wtf` + } catch (p) { + err = p + } + assert.ok(err.exitCode > 0) + assert.ok(err.stderr.includes('wtf: command not found')) + assert.ok(err[inspect.custom]().includes('Command not found')) + }) - test('ProcessPromise: implements toString()', async () => { - const p = $`echo foo` - assert.equal((await p).toString(), 'foo\n') - }) + test('error event is handled', async () => { + await within(async () => { + $.cwd = 'wtf' + try { + await $`pwd` + assert.unreachable('should have thrown') + } catch (err) { + assert.ok(err instanceof ProcessOutput) + assert.match(err.message, /No such file or directory/) + } + }) + }) - test('ProcessPromise: implements json()', async () => { - const p = $`echo '{"key":"value"}'` - assert.deepEqual((await p).json(), { key: 'value' }) - }) + test('await $`cmd`.exitCode does not throw', async () => { + assert.notEqual(await $`grep qwerty README.md`.exitCode, 0) + assert.equal(await $`[[ -f README.md ]]`.exitCode, 0) + }) - test('ProcessPromise: implements text()', async () => { - const p = $`echo foo` - assert.equal((await p).toString(), 'foo\n') - }) + test('`$.sync()` provides synchronous API', () => { + const o1 = $.sync`echo foo` + const o2 = $({ sync: true })`echo foo` + assert.equal(o1.stdout, 'foo\n') + assert.equal(o2.stdout, 'foo\n') + }) - test('ProcessPromise: implements buffer()', async () => { - const p = $`echo foo` - assert.equal((await p).buffer().compare(Buffer.from('foo\n', 'utf-8')), 0) - }) + describe('$({opts}) API', () => { + test('provides presets', async () => { + const $$ = $({ nothrow: true }) + assert.equal((await $$`exit 1`).exitCode, 1) + }) - test('ProcessPromise: implements valueOf()', async () => { - const p = $`echo foo` - assert.equal((await p).valueOf(), 'foo') - assert.ok((await p) == 'foo') - }) + test('handles `input` option', async () => { + const p1 = $({ input: 'foo' })`cat` + const p2 = $({ input: Readable.from('bar') })`cat` + const p3 = $({ input: Buffer.from('baz') })`cat` + const p4 = $({ input: p3 })`cat` + const p5 = $({ input: await p3 })`cat` + + assert.equal((await p1).stdout, 'foo') + assert.equal((await p2).stdout, 'bar') + assert.equal((await p3).stdout, 'baz') + assert.equal((await p4).stdout, 'baz') + assert.equal((await p5).stdout, 'baz') + }) - test('cd() works with relative paths', async () => { - let cwd = process.cwd() - try { - fs.mkdirpSync('/tmp/zx-cd-test/one/two') - cd('/tmp/zx-cd-test/one/two') - let p1 = $`pwd` - assert.equal($.cwd, undefined) - assert.ok(process.cwd().endsWith('/two')) - - cd('..') - let p2 = $`pwd` - assert.equal($.cwd, undefined) - assert.ok(process.cwd().endsWith('/one')) - - cd('..') - let p3 = $`pwd` - assert.equal($.cwd, undefined) - assert.ok(process.cwd().endsWith('/tmp/zx-cd-test')) - - const results = (await Promise.all([p1, p2, p3])).map((p) => - path.basename(p.stdout.trim()) - ) - assert.deepEqual(results, ['two', 'one', 'zx-cd-test']) - } catch (e) { - assert.ok(!e, e) - } finally { - fs.rmSync('/tmp/zx-cd-test', { recursive: true }) - cd(cwd) - } - }) + test('handles `timeout` and `timeoutSignal`', async () => { + let exitCode, signal + try { + await $({ + timeout: 10, + timeoutSignal: 'SIGKILL', + })`sleep 999` + } catch (p) { + exitCode = p.exitCode + signal = p.signal + } + assert.equal(exitCode, null) + assert.equal(signal, 'SIGKILL') + }) + }) - test('cd() does not affect parallel contexts ($.cwdSyncHook enabled)', async () => { - syncProcessCwd() - const cwd = process.cwd() - try { - fs.mkdirpSync('/tmp/zx-cd-parallel/one/two') - await Promise.all([ - within(async () => { - assert.equal(process.cwd(), cwd) - cd('/tmp/zx-cd-parallel/one') - await sleep(Math.random() * 15) - assert.ok(process.cwd().endsWith('/tmp/zx-cd-parallel/one')) - }), - within(async () => { - assert.equal(process.cwd(), cwd) - await sleep(Math.random() * 15) - assert.equal(process.cwd(), cwd) - }), - within(async () => { - assert.equal(process.cwd(), cwd) - await sleep(Math.random() * 15) - $.cwd = '/tmp/zx-cd-parallel/one/two' - assert.equal(process.cwd(), cwd) - assert.ok( - (await $`pwd`).stdout - .toString() - .trim() - .endsWith('/tmp/zx-cd-parallel/one/two') - ) - }), - ]) - } catch (e) { - assert.ok(!e, e) - } finally { - fs.rmSync('/tmp/zx-cd-parallel', { recursive: true }) - cd(cwd) - syncProcessCwd(false) - } - }) + test('accepts `stdio`', async () => { + let p = $({ stdio: 'ignore' })`echo foo` - test('cd() fails on entering not existing dir', async () => { - assert.throws(() => cd('/tmp/abra-kadabra')) + assert.equal((await p).stdout, '') + }) }) - test('cd() accepts ProcessOutput in addition to string', async () => { - await within(async () => { - const tmpDir = await $`mktemp -d` - cd(tmpDir) - assert.equal( - basename(process.cwd()), - basename(tmpDir.toString().trimEnd()) - ) + describe('ProcessPromise', () => { + test('inherits native Promise', async () => { + const p1 = $`echo 1` + const p2 = p1.then((v) => v) + const p3 = p2.then((v) => v) + const p4 = p3.catch((v) => v) + const p5 = p1.finally((v) => v) + + assert(p1 instanceof Promise) + assert(p1 instanceof ProcessPromise) + assert(p2 instanceof ProcessPromise) + assert(p3 instanceof ProcessPromise) + assert(p4 instanceof ProcessPromise) + assert(p5 instanceof ProcessPromise) + assert.ok(p1 !== p2) + assert.ok(p2 !== p3) + assert.ok(p3 !== p4) + assert.ok(p5 !== p1) }) - }) - test('abort() method works', async () => { - const p = $({ detached: true })`sleep 999` - setTimeout(() => p.abort(), 100) + test('resolves with ProcessOutput', async () => { + const o = await $`echo foo` + assert.ok(o instanceof ProcessOutput) + }) - try { + test('stdio() works', async () => { + let p = $`printf foo` await p - assert.unreachable('should have thrown') - } catch ({ message }) { - assert.match(message, /The operation was aborted/) - } - }) + assert.throws(() => p.stdin) + assert.equal((await p).stdout, 'foo') - test('accepts optional AbortController', async () => { - const ac = new AbortController() - const p = $({ ac, detached: true })`sleep 999` - setTimeout(() => ac.abort(), 100) - - try { - await p - assert.unreachable('should have thrown') - } catch ({ message }) { - assert.match(message, /The operation was aborted/) - } - }) + let b = $`read; printf $REPLY` + b.stdin.write('bar\n') + assert.equal((await b).stdout, 'bar') + }) - test('accepts AbortController `signal` separately', async () => { - const ac = new AbortController() - const signal = ac.signal - const p = $({ signal, detached: true })`sleep 999` - setTimeout(() => ac.abort(), 100) + describe('pipe() API', () => { + test('is chainable', async () => { + let { stdout } = await $`echo "hello"` + .pipe($`awk '{print $1" world"}'`) + .pipe($`tr '[a-z]' '[A-Z]'`) + assert.equal(stdout, 'HELLO WORLD\n') + }) - try { - await p - assert.unreachable('should have thrown') - } catch ({ message }) { - assert.match(message, /The operation was aborted/) - } - }) + test('accepts Writable', async () => { + let contents = '' + let stream = new Writable({ + write: function (chunk, encoding, next) { + contents += chunk.toString() + next() + }, + }) + let p = $`echo 'test'`.pipe(stream) + await p + assert.ok(p._piped) + assert.equal(contents, 'test\n') + assert.ok(p.stderr instanceof Socket) + }) - test('kill() method works', async () => { - let p = $`sleep 999`.nothrow() - setTimeout(() => { - p.kill() - }, 100) - await p - }) + test('accepts WriteStream', async () => { + try { + await $`echo foo`.pipe(fs.createWriteStream('/tmp/output.txt')) + assert.equal( + (await fs.readFile('/tmp/output.txt')).toString(), + 'foo\n' + ) - test('a signal is passed with kill() method', async () => { - let p = $`while true; do :; done` - setTimeout(() => p.kill('SIGKILL'), 100) - let signal - try { - await p - } catch (p) { - signal = p.signal - } - assert.equal(signal, 'SIGKILL') - }) + let r = $`cat` + fs.createReadStream('/tmp/output.txt').pipe(r.stdin) + assert.equal((await r).stdout, 'foo\n') + } finally { + await fs.rm('/tmp/output.txt') + } + }) - test('within() works', async () => { - let resolve, reject - let promise = new Promise((...args) => ([resolve, reject] = args)) + test('checks argument type', async () => { + let err + try { + $`echo 'test'`.pipe('str') + } catch (p) { + err = p + } + assert.equal( + err.message, + 'The pipe() method does not take strings. Forgot $?' + ) + }) - function yes() { - assert.equal($.verbose, true) - resolve() - } + test('throws if already resolved', async (t) => { + let ok = true + let p = $`echo "Hello"` + await p + try { + await p.pipe($`less`) + ok = false + } catch (err) { + assert.equal( + err.message, + `The pipe() method shouldn't be called after promise is already resolved!` + ) + } + assert.ok(ok, 'Expected failure!') + }) + }) - assert.equal($.verbose, false) + describe('abort()', () => { + test('just works', async () => { + const p = $({ detached: true })`sleep 999` + setTimeout(() => p.abort(), 100) + + try { + await p + assert.unreachable('should have thrown') + } catch ({ message }) { + assert.match(message, /The operation was aborted/) + } + }) - within(() => { - $.verbose = true - }) - assert.equal($.verbose, false) + test('accepts optional AbortController', async () => { + const ac = new AbortController() + const p = $({ ac, detached: true })`sleep 999` + setTimeout(() => ac.abort(), 100) + + try { + await p + assert.unreachable('should have thrown') + } catch ({ message }) { + assert.match(message, /The operation was aborted/) + } + }) - within(async () => { - $.verbose = true - setTimeout(yes, 10) + test('accepts AbortController `signal` separately', async () => { + const ac = new AbortController() + const signal = ac.signal + const p = $({ signal, detached: true })`sleep 999` + setTimeout(() => ac.abort(), 100) + + try { + await p + assert.unreachable('should have thrown') + } catch ({ message }) { + assert.match(message, /The operation was aborted/) + } + }) }) - assert.equal($.verbose, false) - await promise - }) + describe('kill()', () => { + test('just works', async () => { + let p = $`sleep 999`.nothrow() + setTimeout(() => { + p.kill() + }, 100) + await p + }) - test('within() restores previous cwd', async () => { - let resolve, reject - let promise = new Promise((...args) => ([resolve, reject] = args)) + test('a signal is passed with kill() method', async () => { + let p = $`while true; do :; done` + setTimeout(() => p.kill('SIGKILL'), 100) + let signal + try { + await p + } catch (p) { + signal = p.signal + } + assert.equal(signal, 'SIGKILL') + }) + }) - let pwd = await $`pwd` + test('quiet() mode is working', async () => { + let stdout = '' + let log = console.log + console.log = (...args) => { + stdout += args.join(' ') + } + await $`echo 'test'`.quiet() + console.log = log + assert.equal(stdout, '') + { + // Deprecated. + let stdout = '' + let log = console.log + console.log = (...args) => { + stdout += args.join(' ') + } + await quiet($`echo 'test'`) + console.log = log + assert.equal(stdout, '') + } + }) - within(async () => { - cd('/tmp') - setTimeout(async () => { - assert.ok((await $`pwd`).stdout.trim().endsWith('/tmp')) - resolve() - }, 1000) + test('nothrow() do not throw', async () => { + let { exitCode } = await $`exit 42`.nothrow() + assert.equal(exitCode, 42) + { + // Deprecated. + let { exitCode } = await nothrow($`exit 42`) + assert.equal(exitCode, 42) + } }) - assert.equal((await $`pwd`).stdout, pwd.stdout) - await promise - }) + describe('halt()', () => { + test('just works', async () => { + let filepath = `/tmp/${Math.random().toString()}` + let p = $`touch ${filepath}`.halt() + await sleep(1) + assert.ok( + !fs.existsSync(filepath), + 'The cmd called, but it should not have been called' + ) + await p.run() + assert.ok(fs.existsSync(filepath), 'The cmd should have been called') + }) - test(`within() isolates nested context and returns cb result`, async () => { - within(async () => { - const res = await within(async () => { - $.verbose = true + test('await on halted throws', async () => { + let p = $`sleep 1`.halt() + let ok = true + try { + await p + ok = false + } catch (err) { + assert.equal(err.message, 'The process is halted!') + } + assert.ok(ok, 'Expected failure!') + }) + }) - return within(async () => { - assert.equal($.verbose, true) - $.verbose = false + describe('timeout()', () => { + test('expiration works', async () => { + let exitCode, signal + try { + await $`sleep 1`.timeout(999) + } catch (p) { + exitCode = p.exitCode + signal = p.signal + } + assert.equal(exitCode, undefined) + assert.equal(signal, undefined) + }) - return within(async () => { - assert.equal($.verbose, false) - $.verbose = true - return 'foo' - }) - }) + test('accepts a signal opt', async () => { + let exitCode, signal + try { + await $`sleep 999`.timeout(10, 'SIGKILL') + } catch (p) { + exitCode = p.exitCode + signal = p.signal + } + assert.equal(exitCode, null) + assert.equal(signal, 'SIGKILL') }) - assert.equal($.verbose, false) - assert.equal(res, 'foo') }) }) - test('stdio() works', async () => { - let p = $`printf foo` - await p - assert.throws(() => p.stdin) - assert.equal((await p).stdout, 'foo') + describe('ProcessOutput', () => { + test('implements toString()', async () => { + const p = $`echo foo` + assert.equal((await p).toString(), 'foo\n') + }) - let b = $`read; printf $REPLY` - b.stdin.write('bar\n') - assert.equal((await b).stdout, 'bar') - }) + test('implements valueOf()', async () => { + const p = $`echo foo` + assert.equal((await p).valueOf(), 'foo') + assert.ok((await p) == 'foo') + }) - test('stdio as option', async () => { - let p = $({ stdio: 'ignore' })`echo foo` + test('implements json()', async () => { + const p = $`echo '{"key":"value"}'` + assert.deepEqual((await p).json(), { key: 'value' }) + }) - assert.equal((await p).stdout, '') - }) + test('implements text()', async () => { + const p = $`echo foo` + assert.equal((await p).toString(), 'foo\n') + }) - test('snapshots works', async () => { - await within(async () => { - $.prefix += 'echo success;' - let p = $`:` - $.prefix += 'echo fail;' - let out = await p - assert.equal(out.stdout, 'success\n') - assert.doesNotMatch(out.stdout, /fail/) + test('implements buffer()', async () => { + const p = $`echo foo` + assert.equal((await p).buffer().compare(Buffer.from('foo\n', 'utf-8')), 0) }) }) - test('timeout() works', async () => { - let exitCode, signal - try { - await $`sleep 999`.timeout(10, 'SIGKILL') - } catch (p) { - exitCode = p.exitCode - signal = p.signal - } - assert.equal(exitCode, null) - assert.equal(signal, 'SIGKILL') - }) + describe('cd()', () => { + test('works with relative paths', async () => { + let cwd = process.cwd() + try { + fs.mkdirpSync('/tmp/zx-cd-test/one/two') + cd('/tmp/zx-cd-test/one/two') + let p1 = $`pwd` + assert.equal($.cwd, undefined) + assert.ok(process.cwd().endsWith('/two')) + + cd('..') + let p2 = $`pwd` + assert.equal($.cwd, undefined) + assert.ok(process.cwd().endsWith('/one')) + + cd('..') + let p3 = $`pwd` + assert.equal($.cwd, undefined) + assert.ok(process.cwd().endsWith('/tmp/zx-cd-test')) + + const results = (await Promise.all([p1, p2, p3])).map((p) => + path.basename(p.stdout.trim()) + ) + assert.deepEqual(results, ['two', 'one', 'zx-cd-test']) + } catch (e) { + assert.ok(!e, e) + } finally { + fs.rmSync('/tmp/zx-cd-test', { recursive: true }) + cd(cwd) + } + }) - test('timeout is configurable via opts', async () => { - let exitCode, signal - try { - await $({ - timeout: 10, - timeoutSignal: 'SIGKILL', - })`sleep 999` - } catch (p) { - exitCode = p.exitCode - signal = p.signal - } - assert.equal(exitCode, null) - assert.equal(signal, 'SIGKILL') - }) + test('does not affect parallel contexts ($.cwdSyncHook enabled)', async () => { + syncProcessCwd() + const cwd = process.cwd() + try { + fs.mkdirpSync('/tmp/zx-cd-parallel/one/two') + await Promise.all([ + within(async () => { + assert.equal(process.cwd(), cwd) + cd('/tmp/zx-cd-parallel/one') + await sleep(Math.random() * 15) + assert.ok(process.cwd().endsWith('/tmp/zx-cd-parallel/one')) + }), + within(async () => { + assert.equal(process.cwd(), cwd) + await sleep(Math.random() * 15) + assert.equal(process.cwd(), cwd) + }), + within(async () => { + assert.equal(process.cwd(), cwd) + await sleep(Math.random() * 15) + $.cwd = '/tmp/zx-cd-parallel/one/two' + assert.equal(process.cwd(), cwd) + assert.ok( + (await $`pwd`).stdout + .toString() + .trim() + .endsWith('/tmp/zx-cd-parallel/one/two') + ) + }), + ]) + } catch (e) { + assert.ok(!e, e) + } finally { + fs.rmSync('/tmp/zx-cd-parallel', { recursive: true }) + cd(cwd) + syncProcessCwd(false) + } + }) - test('timeout() expiration works', async () => { - let exitCode, signal - try { - await $`sleep 1`.timeout(999) - } catch (p) { - exitCode = p.exitCode - signal = p.signal - } - assert.equal(exitCode, undefined) - assert.equal(signal, undefined) - }) + test('fails on entering not existing dir', async () => { + assert.throws(() => cd('/tmp/abra-kadabra')) + }) - test('$ thrown as error', async () => { - let err - try { - await $`wtf` - } catch (p) { - err = p - } - assert.ok(err.exitCode > 0) - assert.ok(err.stderr.includes('wtf: command not found')) - assert.ok(err[inspect.custom]().includes('Command not found')) + test('accepts ProcessOutput in addition to string', async () => { + await within(async () => { + const tmpDir = await $`mktemp -d` + cd(tmpDir) + assert.equal( + basename(process.cwd()), + basename(tmpDir.toString().trimEnd()) + ) + }) + }) }) - test('error event is handled', async () => { - await within(async () => { - $.cwd = 'wtf' - try { - await $`pwd` - assert.unreachable('should have thrown') - } catch (err) { - assert.ok(err instanceof ProcessOutput) - assert.match(err.message, /No such file or directory/) + describe('within()', () => { + test('just works', async () => { + let resolve, reject + let promise = new Promise((...args) => ([resolve, reject] = args)) + + function yes() { + assert.equal($.verbose, true) + resolve() } - }) - }) - test('pipe() throws if already resolved', async (t) => { - let ok = true - let p = $`echo "Hello"` - await p - try { - await p.pipe($`less`) - ok = false - } catch (err) { - assert.equal( - err.message, - `The pipe() method shouldn't be called after promise is already resolved!` - ) - } - assert.ok(ok, 'Expected failure!') - }) + assert.equal($.verbose, false) - test('await $`cmd`.exitCode does not throw', async () => { - assert.notEqual(await $`grep qwerty README.md`.exitCode, 0) - assert.equal(await $`[[ -f README.md ]]`.exitCode, 0) - }) + within(() => { + $.verbose = true + }) + assert.equal($.verbose, false) - test('nothrow() do not throw', async () => { - let { exitCode } = await $`exit 42`.nothrow() - assert.equal(exitCode, 42) - { - // Deprecated. - let { exitCode } = await nothrow($`exit 42`) - assert.equal(exitCode, 42) - } - }) + within(async () => { + $.verbose = true + setTimeout(yes, 10) + }) + assert.equal($.verbose, false) - test('malformed cmd error', async () => { - assert.throws(() => $`\033`, /malformed/i) - }) + await promise + }) - test('$ is a regular function', async () => { - const _$ = $.bind(null) - let foo = await _$`echo foo` - assert.equal(foo.stdout, 'foo\n') - assert.ok(typeof $.call === 'function') - assert.ok(typeof $.apply === 'function') - }) + test('restores previous cwd', async () => { + let resolve, reject + let promise = new Promise((...args) => ([resolve, reject] = args)) - test('halt() works', async () => { - let filepath = `/tmp/${Math.random().toString()}` - let p = $`touch ${filepath}`.halt() - await sleep(1) - assert.ok( - !fs.existsSync(filepath), - 'The cmd called, but it should not have been called' - ) - await p.run() - assert.ok(fs.existsSync(filepath), 'The cmd should have been called') - }) + let pwd = await $`pwd` - test('await on halted throws', async () => { - let p = $`sleep 1`.halt() - let ok = true - try { - await p - ok = false - } catch (err) { - assert.equal(err.message, 'The process is halted!') - } - assert.ok(ok, 'Expected failure!') + within(async () => { + cd('/tmp') + setTimeout(async () => { + assert.ok((await $`pwd`).stdout.trim().endsWith('/tmp')) + resolve() + }, 1000) + }) + + assert.equal((await $`pwd`).stdout, pwd.stdout) + await promise + }) + + test(`isolates nested context and returns cb result`, async () => { + within(async () => { + const res = await within(async () => { + $.verbose = true + + return within(async () => { + assert.equal($.verbose, true) + $.verbose = false + + return within(async () => { + assert.equal($.verbose, false) + $.verbose = true + return 'foo' + }) + }) + }) + assert.equal($.verbose, false) + assert.equal(res, 'foo') + }) + }) }) - describe('presets', () => { + describe('shell presets', () => { const originalWhichSync = which.sync before(() => { which.sync = (bin) => bin