From 6b1eb65af4de6b01ad316fa78ccc405c0bff4deb Mon Sep 17 00:00:00 2001 From: Anton Golub Date: Fri, 6 Dec 2024 18:21:45 +0300 Subject: [PATCH] feat: apply formatting to $ error messages --- src/main/ts/error.ts | 6 +- src/main/ts/x.ts | 2 + src/main/ts/zurk.ts | 12 ++- src/test/ts/error.test.ts | 28 ++++++ src/test/ts/x.test.ts | 14 +-- target/cjs/zurk.cjs | 191 +++++++++++++++++++++++++++++++++++++- target/dts/error.d.ts | 4 +- target/esm/zurk.mjs | 191 +++++++++++++++++++++++++++++++++++++- 8 files changed, 428 insertions(+), 20 deletions(-) create mode 100644 src/test/ts/error.test.ts diff --git a/src/main/ts/error.ts b/src/main/ts/error.ts index 6de5a5f..698be75 100644 --- a/src/main/ts/error.ts +++ b/src/main/ts/error.ts @@ -166,7 +166,7 @@ export function getExitCodeInfo(exitCode: number | null): string | undefined { return EXIT_CODES[exitCode as keyof typeof EXIT_CODES] } -export const getExitMessage = ( +export const formatExitMessage = ( code: number | null, signal: NodeJS.Signals | null, stderr: string, @@ -186,7 +186,7 @@ export const getExitMessage = ( return message } -export const getErrorMessage = (err: NodeJS.ErrnoException, from: string) => { +export const formatErrorMessage = (err: NodeJS.ErrnoException, from: string) => { return ( `${err.message}\n` + ` errno: ${err.errno} (${getErrnoMessage(err.errno)})\n` + @@ -195,7 +195,7 @@ export const getErrorMessage = (err: NodeJS.ErrnoException, from: string) => { ) } -export function getCallerLocation(err = new Error()) { +export function getCallerLocation(err = new Error('zurk error')) { return getCallerLocationFromString(err.stack) } diff --git a/src/main/ts/x.ts b/src/main/ts/x.ts index 81b77c7..7253125 100644 --- a/src/main/ts/x.ts +++ b/src/main/ts/x.ts @@ -21,6 +21,7 @@ import { g, immediate } from './util.js' +import { getCallerLocation } from './error.ts' import { pipeMixin } from './mixin/pipe.js' import { killMixin } from './mixin/kill.js' import { timeoutMixin } from './mixin/timeout.js' @@ -90,6 +91,7 @@ export interface TShellSync { export const $: TShell = function(this: any, pieces?: any, ...args: any): any { const self = (this !== g) && this const preset = self || {} + preset.stack = (preset.stack || getCallerLocation()) if (pieces === undefined) return applyMixins($, preset) diff --git a/src/main/ts/zurk.ts b/src/main/ts/zurk.ts index aa505d9..4f145b0 100644 --- a/src/main/ts/zurk.ts +++ b/src/main/ts/zurk.ts @@ -12,6 +12,10 @@ import { type Promisified, type TVoidCallback } from './util.js' +import { + formatErrorMessage, + formatExitMessage +} from './error.js' export const ZURK = Symbol('Zurk') export const ZURKPROXY = Symbol('ZurkProxy') @@ -91,6 +95,7 @@ export const zurkifyPromise = (target: Promise | TZurkPromise, ctx: TSpaw if (p === 'stdio') return ctx.stdio if (p === 'ctx') return ctx if (p === 'child') return ctx.child + if (p === 'stack') return ctx.stack if (p === 'on') return function (name: string, cb: VoidFunction){ ctx.ee.on(name, cb); return proxy } if (p in target) return Reflect.get(target, p, receiver) @@ -103,9 +108,10 @@ export const zurkifyPromise = (target: Promise | TZurkPromise, ctx: TSpaw } export const getError = (data: TSpawnResult): Error | null => { - if (data.error) return data.error - if (data.status) return new Error(`Command failed with exit code ${data.status}`) - if (data.signal) return new Error(`Command failed with signal ${data.signal}`) + if (data.error) + return new Error(formatErrorMessage(data.error, data.stack)) + if (data.status || data.signal) + return new Error(formatExitMessage(data.status, data.signal, data.stderr, data.stack)) return null } diff --git a/src/test/ts/error.test.ts b/src/test/ts/error.test.ts new file mode 100644 index 0000000..28af1e4 --- /dev/null +++ b/src/test/ts/error.test.ts @@ -0,0 +1,28 @@ +import * as assert from 'node:assert' +import { describe, it } from 'node:test' +import { + getCallerLocation, + getCallerLocationFromString, + getExitCodeInfo, + getErrnoMessage, + formatErrorMessage, + formatExitMessage, + EXIT_CODES, + ERRNO_CODES +} from '../../main/ts/error.js' + +import * as all from '../../main/ts/error.js' + +describe('error', () => { + it('has proper exports', () => { + assert.equal(typeof getCallerLocation, 'function') + assert.equal(typeof getCallerLocationFromString, 'function') + assert.equal(typeof getExitCodeInfo, 'function') + assert.equal(typeof getErrnoMessage, 'function') + assert.equal(typeof formatErrorMessage, 'function') + assert.equal(typeof formatExitMessage, 'function') + assert.equal(typeof EXIT_CODES, 'object') + assert.equal(typeof ERRNO_CODES, 'object') + assert.equal(Object.keys(all).length, 8) + }) +}) diff --git a/src/test/ts/x.test.ts b/src/test/ts/x.test.ts index 8be7195..71a149f 100644 --- a/src/test/ts/x.test.ts +++ b/src/test/ts/x.test.ts @@ -25,7 +25,8 @@ describe('$()', () => { try { await $`exit 2` } catch (error: unknown) { - assert.equal((error as Error).message, 'Command failed with exit code 2') + console.error(error) + assert.ok((error as Error).message.includes('exit code: 2 (Misuse of shell builtins)')) } }) @@ -40,7 +41,7 @@ describe('$()', () => { try { $({sync: true})`exit 2` } catch (error: unknown) { - assert.equal((error as Error).message, 'Command failed with exit code 2') + assert.match((error as Error).message, /exit code: 2 \(Misuse of shell builtins\)/) } }) @@ -103,7 +104,7 @@ describe('mixins', () => { const signal = await killed assert.equal(signal, 'SIGTERM') - assert.equal(error.message, 'Command failed with signal SIGTERM') + assert.ok(error.message.includes('signal: SIGTERM')) }) it('handles `abort`', async () => { @@ -119,7 +120,8 @@ describe('mixins', () => { const { error } = await p assert.ok(getEventListeners(p.ctx.signal, 'abort').length < c) - assert.equal(error.message, 'The operation was aborted') + assert.ok(error.message.startsWith('The operation was aborted')) + assert.match(error.message, /code: ABORT_ERR/) assert.deepEqual(events, ['abort', 'end']) }) }) @@ -129,7 +131,7 @@ describe('mixins', () => { const p = $({ timeout: 25, timeoutSignal: 'SIGALRM', nothrow: true })`sleep 10` const { error } = await p - assert.equal(error.message, 'Command failed with signal SIGALRM') + assert.ok(error.message.includes('signal: SIGALRM')) }) it('handles `timeout` as promise setter', async () => { @@ -139,7 +141,7 @@ describe('mixins', () => { p.ctx.nothrow = true const { error } = await p - assert.equal(error.message, 'Command failed with signal SIGALRM') + assert.ok(error.message.includes('signal: SIGALRM')) }) }) diff --git a/target/cjs/zurk.cjs b/target/cjs/zurk.cjs index 7b8fc21..29aec18 100644 --- a/target/cjs/zurk.cjs +++ b/target/cjs/zurk.cjs @@ -25,6 +25,190 @@ __export(zurk_exports, { module.exports = __toCommonJS(zurk_exports); var import_spawn = require("./spawn.cjs"); var import_util = require("./util.cjs"); + +// src/main/ts/error.ts +var EXIT_CODES = { + 2: "Misuse of shell builtins", + 126: "Invoked command cannot execute", + 127: "Command not found", + 128: "Invalid exit argument", + 129: "Hangup", + 130: "Interrupt", + 131: "Quit and dump core", + 132: "Illegal instruction", + 133: "Trace/breakpoint trap", + 134: "Process aborted", + 135: 'Bus error: "access to undefined portion of memory object"', + 136: 'Floating point exception: "erroneous arithmetic operation"', + 137: "Kill (terminate immediately)", + 138: "User-defined 1", + 139: "Segmentation violation", + 140: "User-defined 2", + 141: "Write to pipe with no one reading", + 142: "Signal raised by alarm", + 143: "Termination (request to terminate)", + 145: "Child process terminated, stopped (or continued*)", + 146: "Continue if stopped", + 147: "Stop executing temporarily", + 148: "Terminal stop signal", + 149: 'Background process attempting to read from tty ("in")', + 150: 'Background process attempting to write to tty ("out")', + 151: "Urgent data available on socket", + 152: "CPU time limit exceeded", + 153: "File size limit exceeded", + 154: 'Signal raised by timer counting virtual time: "virtual timer expired"', + 155: "Profiling timer expired", + 157: "Pollable event", + 159: "Bad syscall" +}; +var ERRNO_CODES = { + 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" +}; +function getErrnoMessage(errno) { + return ERRNO_CODES[-errno] || "Unknown error"; +} +function getExitCodeInfo(exitCode) { + return EXIT_CODES[exitCode]; +} +var formatExitMessage = (code, signal, stderr, from) => { + let message = `exit code: ${code}`; + if (code != 0 || signal != null) { + message = `${stderr || "\n"} at ${from}`; + message += ` + exit code: ${code}${getExitCodeInfo(code) ? " (" + getExitCodeInfo(code) + ")" : ""}`; + if (signal != null) { + message += ` + signal: ${signal}`; + } + } + return message; +}; +var formatErrorMessage = (err, from) => { + return `${err.message} + errno: ${err.errno} (${getErrnoMessage(err.errno)}) + code: ${err.code} + at ${from}`; +}; + +// src/main/ts/zurk.ts var ZURK = Symbol("Zurk"); var ZURKPROXY = Symbol("ZurkProxy"); var zurk = (opts) => opts.sync ? zurkSync(opts) : zurkAsync(opts); @@ -77,9 +261,10 @@ var zurkifyPromise = (target, ctx) => { return proxy; }; var getError = (data) => { - if (data.error) return data.error; - if (data.status) return new Error(`Command failed with exit code ${data.status}`); - if (data.signal) return new Error(`Command failed with signal ${data.signal}`); + if (data.error) + return new Error(formatErrorMessage(data.error, data.stack)); + if (data.status || data.signal) + return new Error(formatExitMessage(data.status, data.signal, data.stderr, data.stack)); return null; }; var isZurkAny = (o) => (o == null ? void 0 : o[ZURK]) === ZURK; diff --git a/target/dts/error.d.ts b/target/dts/error.d.ts index 7be5ac8..8c40b9d 100644 --- a/target/dts/error.d.ts +++ b/target/dts/error.d.ts @@ -155,7 +155,7 @@ export declare const ERRNO_CODES: { }; export declare function getErrnoMessage(errno?: number): string; export declare function getExitCodeInfo(exitCode: number | null): string | undefined; -export declare const getExitMessage: (code: number | null, signal: NodeJS.Signals | null, stderr: string, from: string) => string; -export declare const getErrorMessage: (err: NodeJS.ErrnoException, from: string) => string; +export declare const formatExitMessage: (code: number | null, signal: NodeJS.Signals | null, stderr: string, from: string) => string; +export declare const formatErrorMessage: (err: NodeJS.ErrnoException, from: string) => string; export declare function getCallerLocation(err?: Error): string; export declare function getCallerLocationFromString(stackString?: string): string; diff --git a/target/esm/zurk.mjs b/target/esm/zurk.mjs index cb3c387..ae0ec43 100644 --- a/target/esm/zurk.mjs +++ b/target/esm/zurk.mjs @@ -12,6 +12,190 @@ import { isPromiseLike, makeDeferred } from "./util.mjs"; + +// src/main/ts/error.ts +var EXIT_CODES = { + 2: "Misuse of shell builtins", + 126: "Invoked command cannot execute", + 127: "Command not found", + 128: "Invalid exit argument", + 129: "Hangup", + 130: "Interrupt", + 131: "Quit and dump core", + 132: "Illegal instruction", + 133: "Trace/breakpoint trap", + 134: "Process aborted", + 135: 'Bus error: "access to undefined portion of memory object"', + 136: 'Floating point exception: "erroneous arithmetic operation"', + 137: "Kill (terminate immediately)", + 138: "User-defined 1", + 139: "Segmentation violation", + 140: "User-defined 2", + 141: "Write to pipe with no one reading", + 142: "Signal raised by alarm", + 143: "Termination (request to terminate)", + 145: "Child process terminated, stopped (or continued*)", + 146: "Continue if stopped", + 147: "Stop executing temporarily", + 148: "Terminal stop signal", + 149: 'Background process attempting to read from tty ("in")', + 150: 'Background process attempting to write to tty ("out")', + 151: "Urgent data available on socket", + 152: "CPU time limit exceeded", + 153: "File size limit exceeded", + 154: 'Signal raised by timer counting virtual time: "virtual timer expired"', + 155: "Profiling timer expired", + 157: "Pollable event", + 159: "Bad syscall" +}; +var ERRNO_CODES = { + 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" +}; +function getErrnoMessage(errno) { + return ERRNO_CODES[-errno] || "Unknown error"; +} +function getExitCodeInfo(exitCode) { + return EXIT_CODES[exitCode]; +} +var formatExitMessage = (code, signal, stderr, from) => { + let message = `exit code: ${code}`; + if (code != 0 || signal != null) { + message = `${stderr || "\n"} at ${from}`; + message += ` + exit code: ${code}${getExitCodeInfo(code) ? " (" + getExitCodeInfo(code) + ")" : ""}`; + if (signal != null) { + message += ` + signal: ${signal}`; + } + } + return message; +}; +var formatErrorMessage = (err, from) => { + return `${err.message} + errno: ${err.errno} (${getErrnoMessage(err.errno)}) + code: ${err.code} + at ${from}`; +}; + +// src/main/ts/zurk.ts var ZURK = Symbol("Zurk"); var ZURKPROXY = Symbol("ZurkProxy"); var zurk = (opts) => opts.sync ? zurkSync(opts) : zurkAsync(opts); @@ -64,9 +248,10 @@ var zurkifyPromise = (target, ctx) => { return proxy; }; var getError = (data) => { - if (data.error) return data.error; - if (data.status) return new Error(`Command failed with exit code ${data.status}`); - if (data.signal) return new Error(`Command failed with signal ${data.signal}`); + if (data.error) + return new Error(formatErrorMessage(data.error, data.stack)); + if (data.status || data.signal) + return new Error(formatExitMessage(data.status, data.signal, data.stderr, data.stack)); return null; }; var isZurkAny = (o) => (o == null ? void 0 : o[ZURK]) === ZURK;