Skip to content

Commit

Permalink
fix(core): catch of error should work with exitCode and/or code
Browse files Browse the repository at this point in the history
  • Loading branch information
ghiscoding committed Feb 16, 2022
1 parent b897936 commit 461ec29
Show file tree
Hide file tree
Showing 2 changed files with 109 additions and 23 deletions.
89 changes: 89 additions & 0 deletions packages/core/src/__tests__/child-process.spec.ts
@@ -0,0 +1,89 @@
import npmlog from 'npmlog';
// file under test
import { exec, execSync, getChildProcessCount, spawn } from '../child-process';
import { Package } from '../package';

describe('childProcess', () => {
describe('.execSync()', () => {
it('should execute a command in a child process and return the result', () => {
expect(execSync('echo', ['execSync'])).toBe(`"execSync"`);
});

it('should execute a command in dry-run and log the command', () => {
const logSpy = jest.spyOn(npmlog, 'info');
execSync('echo', ['execSync'], undefined, true);
expect(logSpy).toHaveBeenCalledWith('dry-run>', 'echo execSync');
});

it('does not error when stdout is ignored', () => {
expect(() => execSync('echo', ['ignored'], { stdio: 'ignore' })).not.toThrow();
});
});

describe('.exec()', () => {
it('returns an execa Promise', async () => {
const { stderr, stdout } = await exec('echo', ['foo']) as any;

expect(stderr).toBe('');
expect(stdout).toBe(`"foo"`);
});

it('rejects on undefined command', async () => {
const result = exec('nowImTheModelOfAModernMajorGeneral', undefined);

await expect(result).rejects.toThrow(/\bnowImTheModelOfAModernMajorGeneral\b/);
expect(getChildProcessCount()).toBe(0);
});

it('registers child processes that are created', async () => {
const echoOne = exec('echo', ['one']);
expect(getChildProcessCount()).toBe(1);

const echoTwo = exec('echo', ['two']);
expect(getChildProcessCount()).toBe(2);

const [one, two] = await Promise.all([echoOne, echoTwo]) as any;
expect(one.stdout).toBe(`"one"`);
expect(two.stdout).toBe(`"two"`);
});

xit('decorates opts.pkg on error if caught', async () => {
const result = exec(
'theVeneratedVirginianVeteranWhoseMenAreAll',
['liningUpToPutMeUpOnAPedestal'],
{ pkg: { name: 'hamilton' } as Package }
);

await expect(result).rejects.toThrow(
expect.objectContaining({
pkg: { name: 'hamilton' },
})
);
});
});

describe('.spawn()', () => {
it('should spawn a command in a child process that always inherits stdio', async () => {
const child = spawn('echo', ['-n']) as any;
expect(child.stdio).toEqual([null, null, null]);

const { exitCode, signal } = await child;
expect(exitCode).toBe(0);
expect(signal).toBe(undefined);
});

it('decorates opts.pkg on error if caught', async () => {
const result = spawn('exit', ['123'], {
pkg: { name: 'shelled' } as Package,
shell: true,
});

await expect(result).rejects.toThrow(
expect.objectContaining({
exitCode: 123,
pkg: { name: 'shelled' },
})
);
});
});
});
43 changes: 20 additions & 23 deletions packages/core/src/child-process.ts
Expand Up @@ -3,7 +3,8 @@ import execa from 'execa';
import log from 'npmlog';
import os from 'os';
import logTransformer from 'strong-log-transformer';
import { Package } from '.';

import { Package } from './package';

// bookkeeping for spawned processes
const children = new Set();
Expand All @@ -15,29 +16,29 @@ const NUM_COLORS = colorWheel.length;
// ever-increasing index ensures colors are always sequential
let currentColor = 0;

export function exec(command: string, args: string[], opts: any, cmdDryRun = false) {
export function exec(command: string, args: string[], opts?: execa.Options & { pkg?: Package }, cmdDryRun = false) {
const options = Object.assign({ stdio: 'pipe' }, opts);
const spawned = spawnProcess(command, args, options, cmdDryRun);

return cmdDryRun ? Promise.resolve() : wrapError(spawned);
}

// resultCallback?: (processResult: ChildProcessResult) => void
export function execSync(command: string, args?: string[], opts?: any, cmdDryRun = false) {
export function execSync(command: string, args?: string[], opts?: execa.SyncOptions<string>, cmdDryRun = false) {
return cmdDryRun
? logExecCommand(command, args)
: execa.sync(command, args, opts).stdout;
}

export function spawn(command: string, args: string[], opts: any, cmdDryRun = false) {
export function spawn(command: string, args: string[], opts?: execa.Options & { pkg?: Package }, cmdDryRun = false) {
const options = Object.assign({}, opts, { stdio: 'inherit' });
const spawned = spawnProcess(command, args, options, cmdDryRun);

return wrapError(spawned);
}

// istanbul ignore next
export function spawnStreaming(command: string, args: string[], opts: any, prefix?: string | boolean, cmdDryRun = false) {
export function spawnStreaming(command: string, args: string[], opts?: execa.Options & { pkg?: Package }, prefix?: string | boolean, cmdDryRun = false) {
const options: any = Object.assign({}, opts);
options.stdio = ['ignore', 'pipe', 'pipe'];

Expand Down Expand Up @@ -74,21 +75,21 @@ export function getChildProcessCount() {

export function getExitCode(result: any) {
// https://nodejs.org/docs/latest-v6.x/api/child_process.html#child_process_event_close
if (typeof result.code === 'number') {
return result.code;
if (typeof result.code === 'number' || typeof result.exitCode === 'number') {
return result.code ?? result.exitCode;
}

// https://nodejs.org/docs/latest-v6.x/api/errors.html#errors_error_code
// istanbul ignore else
if (typeof result.code === 'string') {
return os.constants.errno[result.code];
if (typeof result.code === 'string' || typeof result.exitCode === 'string') {
return os.constants.errno[result.code ?? result.exitCode];
}

// istanbul ignore next: extremely weird
throw new TypeError(`Received unexpected exit code value ${JSON.stringify(result.code)}`);
throw new TypeError(`Received unexpected exit code value ${JSON.stringify(result.code ?? result.exitCode)}`);
}

export function spawnProcess(command: string, args: string[], opts: execa.SyncOptions<string> & { pkg: Package }, cmdDryRun = false) {
export function spawnProcess(command: string, args: string[], opts: execa.Options & { pkg?: Package }, cmdDryRun = false) {
if (cmdDryRun) {
return logExecCommand(command, args);
}
Expand All @@ -114,17 +115,14 @@ export function spawnProcess(command: string, args: string[], opts: execa.SyncOp
return child;
}

export function wrapError(spawned) {
export function wrapError(spawned: execa.ExecaChildProcess & { pkg?: Package }) {
if (spawned.pkg) {
return spawned.catch(err => {
// istanbul ignore else
if (err.code) {
// ensure code is always a number
err.code = getExitCode(err);
return spawned.catch((err: any) => {
// ensure exit code is always a number
err.exitCode = getExitCode(err);

// log non-lerna error cleanly
err.pkg = spawned.pkg;
}
// log non-lerna error cleanly
err.pkg = spawned.pkg;

throw err;
});
Expand All @@ -133,12 +131,11 @@ export function wrapError(spawned) {
return spawned;
}

export function logExecCommand(command, args?: string[]) {
export function logExecCommand(command: string, args?: string[]) {
const argStr = (Array.isArray(args) ? args.join(' ') : args) ?? '';

const cmdList = [];
const cmdList: string[] = [];
for (const c of [command, argStr]) {
// @ts-ignore
cmdList.push((Array.isArray(c) ? c.join(' ') : c));
}

Expand Down

0 comments on commit 461ec29

Please sign in to comment.