Commit f14d8b2
Changed files (4)
test
src/core.ts
@@ -22,6 +22,7 @@ import { chalk, which } from './goods.js'
import { log } from './log.js'
import {
Duration,
+ errnoMessage,
exitCodeInfo,
noop,
parseDuration,
@@ -193,6 +194,17 @@ export class ProcessPromise extends Promise<ProcessOutput> {
}
this._resolved = true
})
+ this.child.on('error', (err: NodeJS.ErrnoException) => {
+ const message =
+ `${err.message}\n` +
+ ` errno: ${err.errno} (${errnoMessage(err.errno)})\n` +
+ ` code: ${err.code}\n` +
+ ` at ${this._from}`
+ this._reject(
+ new ProcessOutput(null, null, stdout, stderr, combined, message)
+ )
+ this._resolved = true
+ })
let stdout = '',
stderr = '',
combined = ''
src/util.ts
@@ -99,6 +99,135 @@ export function exitCodeInfo(exitCode: number | null): string | undefined {
}[exitCode || -1]
}
+export function errnoMessage(errno: number | undefined): string {
+ if (errno === undefined) {
+ return 'Unknown error'
+ }
+ return (
+ {
+ 0: 'Success',
+ 1: 'Not super-user',
+ 2: 'No such file or directory',
+ 3: 'No such process',
+ 4: 'Interrupted system call',
+ 5: 'I/O error',
+ 6: 'No such device or address',
+ 7: 'Arg list too long',
+ 8: 'Exec format error',
+ 9: 'Bad file number',
+ 10: 'No children',
+ 11: 'No more processes',
+ 12: 'Not enough core',
+ 13: 'Permission denied',
+ 14: 'Bad address',
+ 15: 'Block device required',
+ 16: 'Mount device busy',
+ 17: 'File exists',
+ 18: 'Cross-device link',
+ 19: 'No such device',
+ 20: 'Not a directory',
+ 21: 'Is a directory',
+ 22: 'Invalid argument',
+ 23: 'Too many open files in system',
+ 24: 'Too many open files',
+ 25: 'Not a typewriter',
+ 26: 'Text file busy',
+ 27: 'File too large',
+ 28: 'No space left on device',
+ 29: 'Illegal seek',
+ 30: 'Read only file system',
+ 31: 'Too many links',
+ 32: 'Broken pipe',
+ 33: 'Math arg out of domain of func',
+ 34: 'Math result not representable',
+ 35: 'File locking deadlock error',
+ 36: 'File or path name too long',
+ 37: 'No record locks available',
+ 38: 'Function not implemented',
+ 39: 'Directory not empty',
+ 40: 'Too many symbolic links',
+ 42: 'No message of desired type',
+ 43: 'Identifier removed',
+ 44: 'Channel number out of range',
+ 45: 'Level 2 not synchronized',
+ 46: 'Level 3 halted',
+ 47: 'Level 3 reset',
+ 48: 'Link number out of range',
+ 49: 'Protocol driver not attached',
+ 50: 'No CSI structure available',
+ 51: 'Level 2 halted',
+ 52: 'Invalid exchange',
+ 53: 'Invalid request descriptor',
+ 54: 'Exchange full',
+ 55: 'No anode',
+ 56: 'Invalid request code',
+ 57: 'Invalid slot',
+ 59: 'Bad font file fmt',
+ 60: 'Device not a stream',
+ 61: 'No data (for no delay io)',
+ 62: 'Timer expired',
+ 63: 'Out of streams resources',
+ 64: 'Machine is not on the network',
+ 65: 'Package not installed',
+ 66: 'The object is remote',
+ 67: 'The link has been severed',
+ 68: 'Advertise error',
+ 69: 'Srmount error',
+ 70: 'Communication error on send',
+ 71: 'Protocol error',
+ 72: 'Multihop attempted',
+ 73: 'Cross mount point (not really error)',
+ 74: 'Trying to read unreadable message',
+ 75: 'Value too large for defined data type',
+ 76: 'Given log. name not unique',
+ 77: 'f.d. invalid for this operation',
+ 78: 'Remote address changed',
+ 79: 'Can access a needed shared lib',
+ 80: 'Accessing a corrupted shared lib',
+ 81: '.lib section in a.out corrupted',
+ 82: 'Attempting to link in too many libs',
+ 83: 'Attempting to exec a shared library',
+ 84: 'Illegal byte sequence',
+ 86: 'Streams pipe error',
+ 87: 'Too many users',
+ 88: 'Socket operation on non-socket',
+ 89: 'Destination address required',
+ 90: 'Message too long',
+ 91: 'Protocol wrong type for socket',
+ 92: 'Protocol not available',
+ 93: 'Unknown protocol',
+ 94: 'Socket type not supported',
+ 95: 'Not supported',
+ 96: 'Protocol family not supported',
+ 97: 'Address family not supported by protocol family',
+ 98: 'Address already in use',
+ 99: 'Address not available',
+ 100: 'Network interface is not configured',
+ 101: 'Network is unreachable',
+ 102: 'Connection reset by network',
+ 103: 'Connection aborted',
+ 104: 'Connection reset by peer',
+ 105: 'No buffer space available',
+ 106: 'Socket is already connected',
+ 107: 'Socket is not connected',
+ 108: "Can't send after socket shutdown",
+ 109: 'Too many references',
+ 110: 'Connection timed out',
+ 111: 'Connection refused',
+ 112: 'Host is down',
+ 113: 'Host is unreachable',
+ 114: 'Socket already connected',
+ 115: 'Connection already in progress',
+ 116: 'Stale file handle',
+ 122: 'Quota exceeded',
+ 123: 'No medium (in tape drive)',
+ 125: 'Operation canceled',
+ 130: 'Previous owner died',
+ 131: 'State not recoverable',
+ }[-errno] || 'Unknown error'
+ )
+}
+
export type Duration = number | `${number}s` | `${number}ms`
export function parseDuration(d: Duration) {
@@ -137,6 +266,7 @@ export function formatCmd(cmd?: string): string {
state = next == root ? next() : next
buf += ch
}
+
function style(state: State, s: string): string {
if (s == '') return ''
if (reservedWords.includes(s)) {
@@ -154,9 +284,11 @@ export function formatCmd(cmd?: string): string {
if (state?.name.startsWith('str')) return chalk.yellowBright(s)
return s
}
+
function isSyntax(ch: string) {
return '()[]{}<>;:+|&='.includes(ch)
}
+
function root() {
if (/\s/.test(ch)) return space
if (isSyntax(ch)) return syntax
@@ -165,44 +297,55 @@ export function formatCmd(cmd?: string): string {
if (/[']/.test(ch)) return strSingle
return word
}
+
function space() {
if (/\s/.test(ch)) return space
return root
}
+
function word() {
if (/[0-9a-z/_.]/i.test(ch)) return word
return root
}
+
function syntax() {
if (isSyntax(ch)) return syntax
return root
}
+
function dollar() {
if (/[']/.test(ch)) return str
return root
}
+
function str() {
if (/[']/.test(ch)) return strEnd
if (/[\\]/.test(ch)) return strBackslash
return str
}
+
function strBackslash() {
return strEscape
}
+
function strEscape() {
return str
}
+
function strDouble() {
if (/["]/.test(ch)) return strEnd
return strDouble
}
+
function strSingle() {
if (/[']/.test(ch)) return strEnd
return strSingle
}
+
function strEnd() {
return root
}
+
return out + '\n'
}
test/core.test.js
@@ -18,7 +18,7 @@ import * as assert from 'uvu/assert'
import { inspect } from 'node:util'
import { Writable } from 'node:stream'
import { Socket } from 'node:net'
-import { ProcessPromise } from '../build/index.js'
+import { ProcessPromise, ProcessOutput } from '../build/index.js'
import '../build/globals.js'
const test = suite('core')
@@ -166,49 +166,6 @@ test('ProcessPromise: inherits native Promise', async () => {
assert.ok(p5 !== p1)
})
-test('ProcessOutput thrown as error', async () => {
- let err
- try {
- await $`wtf`
- } catch (p) {
- err = p
- }
- assert.ok(err.exitCode > 0)
- assert.ok(err.stderr.includes('/bin/bash: wtf: command not found\n'))
- assert.ok(err[inspect.custom]().includes('Command not found'))
-})
-
-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.is(
- err.message,
- `The pipe() method shouldn't be called after promise is already resolved!`
- )
- }
- assert.ok(ok, 'Expected failure!')
-})
-
-test('await $`cmd`.exitCode does not throw', async () => {
- assert.is.not(await $`grep qwerty README.md`.exitCode, 0)
- assert.is(await $`[[ -f README.md ]]`.exitCode, 0)
-})
-
-test('nothrow() do not throw', async () => {
- let { exitCode } = await $`exit 42`.nothrow()
- assert.is(exitCode, 42)
- {
- // Deprecated.
- let { exitCode } = await nothrow($`exit 42`)
- assert.is(exitCode, 42)
- }
-})
-
test('cd() works with relative paths', async () => {
let cwd = process.cwd()
try {
@@ -408,6 +365,62 @@ test('timeout() expiration works', async () => {
assert.is(signal, undefined)
})
+test('$ thrown as error', async () => {
+ let err
+ try {
+ await $`wtf`
+ } catch (p) {
+ err = p
+ }
+ assert.ok(err.exitCode > 0)
+ assert.ok(err.stderr.includes('/bin/bash: wtf: command not found\n'))
+ assert.ok(err[inspect.custom]().includes('Command not found'))
+})
+
+test('error event is handled', async () => {
+ await within(async () => {
+ $.cwd = 'wtf'
+ try {
+ await $`pwd`
+ assert.unreachable('should have thrown')
+ } catch (err) {
+ assert.instance(err, ProcessOutput)
+ assert.match(err.message, /No such file or directory/)
+ }
+ })
+})
+
+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.is(
+ err.message,
+ `The pipe() method shouldn't be called after promise is already resolved!`
+ )
+ }
+ assert.ok(ok, 'Expected failure!')
+})
+
+test('await $`cmd`.exitCode does not throw', async () => {
+ assert.is.not(await $`grep qwerty README.md`.exitCode, 0)
+ assert.is(await $`[[ -f README.md ]]`.exitCode, 0)
+})
+
+test('nothrow() do not throw', async () => {
+ let { exitCode } = await $`exit 42`.nothrow()
+ assert.is(exitCode, 42)
+ {
+ // Deprecated.
+ let { exitCode } = await nothrow($`exit 42`)
+ assert.is(exitCode, 42)
+ }
+})
+
test('malformed cmd error', async () => {
assert.throws(() => $`\033`, /malformed/i)
})
test/util.test.js
@@ -16,6 +16,7 @@ import { suite } from 'uvu'
import * as assert from 'uvu/assert'
import {
exitCodeInfo,
+ errnoMessage,
formatCmd,
isString,
noop,
@@ -27,7 +28,13 @@ import {
const test = suite('util')
test('exitCodeInfo()', () => {
- assert.ok(exitCodeInfo(2) === 'Misuse of shell builtins')
+ assert.is(exitCodeInfo(2), 'Misuse of shell builtins')
+})
+
+test('errnoMessage()', () => {
+ assert.is(errnoMessage(-2), 'No such file or directory')
+ assert.is(errnoMessage(1e9), 'Unknown error')
+ assert.is(errnoMessage(undefined), 'Unknown error')
})
test('randomId()', () => {