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