v6
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 { test } from 'uvu'
16import * as assert from 'uvu/assert'
17import { inspect } from 'node:util'
18import chalk from 'chalk'
19import { Writable } from 'node:stream'
20import { Socket } from 'node:net'
21import '../build/globals.js'
22import { ProcessPromise } from '../build/index.js'
23import { getCtx, runInCtx } from '../build/context.js'
24
25$.verbose = false
26
27test('only stdout is used during command substitution', async () => {
28 let hello = await $`echo Error >&2; echo Hello`
29 let len = +(await $`echo ${hello} | wc -c`)
30 assert.is(len, 6)
31})
32
33test('env vars works', async () => {
34 process.env.ZX_TEST_FOO = 'foo'
35 let foo = await $`echo $ZX_TEST_FOO`
36 assert.is(foo.stdout, 'foo\n')
37})
38
39test('env vars is safe to pass', async () => {
40 process.env.ZX_TEST_BAR = 'hi; exit 1'
41 await $`echo $ZX_TEST_BAR`
42})
43
44test('arguments are quoted', async () => {
45 let bar = 'bar"";baz!$#^$\'&*~*%)({}||\\/'
46 assert.is((await $`echo ${bar}`).stdout.trim(), bar)
47})
48
49test('undefined and empty string correctly quoted', async () => {
50 $.verbose = true
51 assert.is((await $`echo -n ${undefined}`).toString(), 'undefined')
52 assert.is((await $`echo -n ${''}`).toString(), '')
53 $.verbose = false
54})
55
56test('can create a dir with a space in the name', async () => {
57 let name = 'foo bar'
58 try {
59 await $`mkdir /tmp/${name}`
60 } catch {
61 assert.unreachable()
62 } finally {
63 await fs.rmdir('/tmp/' + name)
64 }
65})
66
67test('pipefail is on', async () => {
68 let p
69 try {
70 p = await $`cat /dev/not_found | sort`
71 } catch (e) {
72 console.log('Caught an exception -> ok')
73 p = e
74 }
75 assert.is.not(p.exitCode, 0)
76})
77
78test('toString() is called on arguments', async () => {
79 let foo = 0
80 let p = await $`echo ${foo}`
81 assert.is(p.stdout, '0\n')
82})
83
84test('can use array as an argument', async () => {
85 let args = ['-n', 'foo']
86 assert.is((await $`echo ${args}`).toString(), 'foo')
87})
88
89test('quiet mode is working', async () => {
90 let stdout = ''
91 let log = console.log
92 console.log = (...args) => {
93 stdout += args.join(' ')
94 }
95 await quiet($`echo 'test'`)
96 console.log = log
97 assert.is(stdout, '')
98})
99
100test('pipes are working', async () => {
101 let { stdout } = await $`echo "hello"`
102 .pipe($`awk '{print $1" world"}'`)
103 .pipe($`tr '[a-z]' '[A-Z]'`)
104 assert.is(stdout, 'HELLO WORLD\n')
105
106 try {
107 await $`echo foo`.pipe(fs.createWriteStream('/tmp/output.txt'))
108 assert.is((await fs.readFile('/tmp/output.txt')).toString(), 'foo\n')
109
110 let r = $`cat`
111 fs.createReadStream('/tmp/output.txt').pipe(r.stdin)
112 assert.is((await r).stdout, 'foo\n')
113 } finally {
114 await fs.rm('/tmp/output.txt')
115 }
116})
117
118test('question', async () => {
119 let p = question('foo or bar? ', { choices: ['foo', 'bar'] })
120
121 setImmediate(() => {
122 process.stdin.emit('data', 'fo')
123 process.stdin.emit('data', '\t')
124 process.stdin.emit('data', '\n')
125 })
126
127 assert.is(await p, 'foo')
128})
129
130test('ProcessPromise', async () => {
131 let contents = ''
132 let stream = new Writable({
133 write: function (chunk, encoding, next) {
134 contents += chunk.toString()
135 next()
136 },
137 })
138 let p = $`echo 'test'`.pipe(stream)
139 await p
140 assert.ok(p._piped)
141 assert.is(contents, 'test\n')
142 assert.instance(p.stderr, Socket)
143
144 let err
145 try {
146 $`echo 'test'`.pipe('str')
147 } catch (p) {
148 err = p
149 }
150 assert.is(err.message, 'The pipe() method does not take strings. Forgot $?')
151})
152
153test('ProcessPromise: inherits native Promise', async () => {
154 const p1 = $`echo 1`
155 const p2 = p1.then((v) => v)
156 const p3 = p2.then((v) => v)
157 const p4 = p3.catch((v) => v)
158 const p5 = p1.finally((v) => v)
159
160 assert.instance(p1, Promise)
161 assert.instance(p1, ProcessPromise)
162 assert.instance(p2, ProcessPromise)
163 assert.instance(p3, ProcessPromise)
164 assert.instance(p4, ProcessPromise)
165 assert.instance(p5, ProcessPromise)
166 assert.ok(p1 !== p2)
167 assert.ok(p2 !== p3)
168 assert.ok(p3 !== p4)
169 assert.ok(p5 !== p1)
170
171 assert.ok(p1.ctx)
172 assert.ok(p2.ctx)
173 assert.ok(p3.ctx)
174 assert.ok(p4.ctx)
175 assert.ok(p5.ctx)
176
177 assert.not.equal(p1.ctx, p2.ctx)
178 assert.equal(p2.ctx, p3.ctx)
179 assert.equal(p3.ctx, p4.ctx)
180 assert.equal(p4.ctx, p5.ctx)
181})
182
183test('ProcessPromise: ctx is protected from removal', async () => {
184 const p = $`echo 1`
185
186 try {
187 delete p.ctx
188 assert.unreachable()
189 } catch (e) {
190 assert.match(e.message, /Cannot delete property/)
191 }
192})
193
194test('ProcessOutput thrown as error', async () => {
195 let err
196 try {
197 await $`unknown`
198 } catch (p) {
199 err = p
200 }
201 assert.ok(err.exitCode > 0)
202 assert.match(err.stderr, /unknown: command not found/)
203 assert.ok(err[inspect.custom]().includes('Command not found'))
204})
205
206test('pipe() throws if already resolved', async (t) => {
207 let out,
208 p = $`echo "Hello"`
209 await p
210 try {
211 out = await p.pipe($`less`)
212 } catch (err) {
213 assert.is(
214 err.message,
215 `The pipe() method shouldn't be called after promise is already resolved!`
216 )
217 }
218 if (out) {
219 t.fail('Expected failure!')
220 }
221})
222
223test('await $`cmd`.exitCode does not throw', async () => {
224 assert.is.not(await $`grep qwerty README.md`.exitCode, 0)
225 assert.is(await $`[[ -f README.md ]]`.exitCode, 0)
226})
227
228test('nothrow() do not throw', async () => {
229 let { exitCode } = await nothrow($`exit 42`)
230 assert.is(exitCode, 42)
231})
232
233test('globby available', async () => {
234 assert.is(globby, glob)
235 assert.is(typeof globby, 'function')
236 assert.is(typeof globby.globbySync, 'function')
237 assert.is(typeof globby.globbyStream, 'function')
238 assert.is(typeof globby.generateGlobTasks, 'function')
239 assert.is(typeof globby.isDynamicPattern, 'function')
240 assert.is(typeof globby.isGitIgnored, 'function')
241 assert.is(typeof globby.isGitIgnoredSync, 'function')
242 assert.equal(await globby('*.md'), ['README.md'])
243})
244
245test('fetch', async () => {
246 assert.match(
247 await fetch('https://medv.io').then((res) => res.text()),
248 /Anton Medvedev/
249 )
250})
251
252test('executes a script from $PATH', async () => {
253 const isWindows = process.platform === 'win32'
254 const oldPath = process.env.PATH
255
256 const envPathSeparator = isWindows ? ';' : ':'
257 process.env.PATH += envPathSeparator + path.resolve('/tmp/')
258
259 const toPOSIXPath = (_path) => _path.split(path.sep).join(path.posix.sep)
260
261 const zxPath = path.resolve('./build/cli.js')
262 const zxLocation = isWindows ? toPOSIXPath(zxPath) : zxPath
263 const scriptCode = `#!/usr/bin/env ${zxLocation}\nconsole.log('The script from path runs.')`
264
265 try {
266 await $`chmod +x ${zxLocation}`
267 await $`echo ${scriptCode}`.pipe(
268 fs.createWriteStream('/tmp/script-from-path', { mode: 0o744 })
269 )
270 await $`script-from-path`
271 } finally {
272 process.env.PATH = oldPath
273 fs.rmSync('/tmp/script-from-path')
274 }
275})
276
277test('cd() works with relative paths', async () => {
278 let cwd = process.cwd()
279 assert.equal($.cwd, cwd)
280 try {
281 fs.mkdirpSync('/tmp/zx-cd-test/one/two')
282 cd('/tmp/zx-cd-test/one/two')
283 let p1 = $`pwd`
284 assert.ok($.cwd.endsWith('/two'))
285 assert.ok(process.cwd().endsWith('/two'))
286
287 cd('..')
288 let p2 = $`pwd`
289 assert.ok($.cwd.endsWith('/one'))
290 assert.ok(process.cwd().endsWith('/one'))
291
292 cd('..')
293 let p3 = $`pwd`
294 assert.ok(process.cwd().endsWith('/zx-cd-test'))
295 assert.ok($.cwd.endsWith('/tmp/zx-cd-test'))
296
297 let results = (await Promise.all([p1, p2, p3])).map((p) =>
298 path.basename(p.stdout.trim())
299 )
300
301 assert.equal(results, ['two', 'one', 'zx-cd-test'])
302 } catch (e) {
303 assert.ok(!e, e)
304 } finally {
305 fs.rmSync('/tmp/zx-cd-test', { recursive: true })
306 cd(cwd)
307 assert.equal($.cwd, cwd)
308 }
309})
310
311test('cd() does not affect parallel contexts', async () => {
312 let cwd = process.cwd()
313 let resolve, reject
314 let promise = new ProcessPromise((...args) => ([resolve, reject] = args))
315
316 try {
317 fs.mkdirpSync('/tmp/zx-cd-parallel')
318 runInCtx({ ...getCtx() }, async () => {
319 assert.equal($.cwd, cwd)
320 await sleep(10)
321 cd('/tmp/zx-cd-parallel')
322 assert.ok(getCtx().cwd.endsWith('/zx-cd-parallel'))
323 assert.ok($.cwd.endsWith('/zx-cd-parallel'))
324 })
325
326 runInCtx({ ...getCtx() }, async () => {
327 assert.equal($.cwd, cwd)
328 assert.equal(getCtx().cwd, cwd)
329 await sleep(20)
330 assert.equal(getCtx().cwd, cwd)
331 assert.ok($.cwd.endsWith('/zx-cd-parallel'))
332 resolve()
333 })
334
335 await promise
336 } catch (e) {
337 assert.ok(!e, e)
338 } finally {
339 fs.rmSync('/tmp/zx-cd-parallel', { recursive: true })
340 cd(cwd)
341 }
342})
343
344test('kill() method works', async () => {
345 let p = nothrow($`sleep 9999`)
346 setTimeout(() => {
347 p.kill()
348 }, 100)
349 await p
350})
351
352test('a signal is passed with kill() method', async () => {
353 let p = $`while true; do :; done`
354 setTimeout(() => p.kill('SIGKILL'), 100)
355 let signal
356 try {
357 await p
358 } catch (p) {
359 signal = p.signal
360 }
361 assert.equal(signal, 'SIGKILL')
362})
363
364test('YAML works', async () => {
365 assert.equal(YAML.parse(YAML.stringify({ foo: 'bar' })), { foo: 'bar' })
366})
367
368test('which available', async () => {
369 assert.is(which.sync('npm'), await which('npm'))
370})
371
372test.run()