diff --git a/e2e/nx-plugin-e2e/tests/executor-cli.e2e.test.ts b/e2e/nx-plugin-e2e/tests/executor-cli.e2e.test.ts index b218b711f..b482c717b 100644 --- a/e2e/nx-plugin-e2e/tests/executor-cli.e2e.test.ts +++ b/e2e/nx-plugin-e2e/tests/executor-cli.e2e.test.ts @@ -1,7 +1,7 @@ import { type Tree, updateProjectConfiguration } from '@nx/devkit'; import path from 'node:path'; import { readProjectConfiguration } from 'nx/src/generators/utils/project-configuration'; -import { afterAll, afterEach, beforeEach, expect, vi } from 'vitest'; +import { afterAll, afterEach, beforeEach, expect } from 'vitest'; import { type AutorunCommandExecutorOptions, generateCodePushupConfig, @@ -17,7 +17,7 @@ import { removeColorCodes, teardownTestFolder, } from '@code-pushup/test-utils'; -import { executeProcess, readJsonFile } from '@code-pushup/utils'; +import { executeProcess, logger, readJsonFile } from '@code-pushup/utils'; import { INLINE_PLUGIN } from '../mocks/inline-plugin.js'; async function addTargetToWorkspace( @@ -62,15 +62,16 @@ describe('executor command', () => { Object.entries(process.env).filter(([k]) => k.startsWith('CP_')), ); - /* eslint-disable functional/immutable-data, @typescript-eslint/no-dynamic-delete */ beforeAll(() => { Object.entries(process.env) .filter(([k]) => k.startsWith('CP_')) - .forEach(([k]) => delete process.env[k]); + .forEach(([k]) => { + // eslint-disable-next-line functional/immutable-data, @typescript-eslint/no-dynamic-delete + delete process.env[k]; + }); }); beforeEach(async () => { - vi.unstubAllEnvs(); tree = await generateWorkspaceAndProject(project); }); @@ -79,9 +80,11 @@ describe('executor command', () => { }); afterAll(() => { - Object.entries(processEnvCP).forEach(([k, v]) => (process.env[k] = v)); + Object.entries(processEnvCP).forEach(([k, v]) => { + // eslint-disable-next-line functional/immutable-data + process.env[k] = v; + }); }); - /* eslint-enable functional/immutable-data, @typescript-eslint/no-dynamic-delete */ it('should execute no specific command by default', async () => { const cwd = path.join(testFileDir, 'execute-default-command'); @@ -179,6 +182,7 @@ describe('executor command', () => { }, }, ); + logger.setVerbose(true); const { stdout, code } = await executeProcess({ command: 'npx', diff --git a/packages/ci/README.md b/packages/ci/README.md index 23d277d05..12f1b4917 100644 --- a/packages/ci/README.md +++ b/packages/ci/README.md @@ -103,7 +103,6 @@ Optionally, you can override default options for further customization: | `nxProjectsFilter` | `string \| string[]` | `'--with-target={task}'` | Arguments passed to [`nx show projects`](https://nx.dev/nx-api/nx/documents/show#projects), only relevant for Nx in [monorepo mode](#monorepo-mode) [^2] | | `directory` | `string` | `process.cwd()` | Directory in which Code PushUp CLI should run | | `config` | `string \| null` | `null` [^1] | Path to config file (`--config` option) | -| `silent` | `boolean` | `false` | Hides logs from CLI commands (errors will be printed) | | `bin` | `string` | `'npx --no-install code-pushup'` | Command for executing Code PushUp CLI | | `detectNewIssues` | `boolean` | `true` | Toggles if new issues should be detected and returned in `newIssues` property | | `skipComment` | `boolean` | `false` | Toggles if comparison comment is posted to PR | diff --git a/packages/ci/src/lib/cli/commands/collect.ts b/packages/ci/src/lib/cli/commands/collect.ts index 3428e91dc..b3c70c0b0 100644 --- a/packages/ci/src/lib/cli/commands/collect.ts +++ b/packages/ci/src/lib/cli/commands/collect.ts @@ -3,7 +3,7 @@ import { executeProcess } from '@code-pushup/utils'; import type { CommandContext } from '../context.js'; export async function runCollect( - { bin, config, directory, observer }: CommandContext, + { bin, config, directory }: CommandContext, { hasFormats }: { hasFormats: boolean }, ): Promise { await executeProcess({ @@ -15,6 +15,5 @@ export async function runCollect( : DEFAULT_PERSIST_FORMAT.map(format => `--persist.format=${format}`)), ], cwd: directory, - observer, }); } diff --git a/packages/ci/src/lib/cli/commands/compare.ts b/packages/ci/src/lib/cli/commands/compare.ts index 641b9c300..993483dc9 100644 --- a/packages/ci/src/lib/cli/commands/compare.ts +++ b/packages/ci/src/lib/cli/commands/compare.ts @@ -3,7 +3,7 @@ import { executeProcess } from '@code-pushup/utils'; import type { CommandContext } from '../context.js'; export async function runCompare( - { bin, config, directory, observer }: CommandContext, + { bin, config, directory }: CommandContext, { hasFormats }: { hasFormats: boolean }, ): Promise { await executeProcess({ @@ -16,6 +16,5 @@ export async function runCompare( : DEFAULT_PERSIST_FORMAT.map(format => `--persist.format=${format}`)), ], cwd: directory, - observer, }); } diff --git a/packages/ci/src/lib/cli/commands/merge-diffs.ts b/packages/ci/src/lib/cli/commands/merge-diffs.ts index 8f24208cb..dd2effb22 100644 --- a/packages/ci/src/lib/cli/commands/merge-diffs.ts +++ b/packages/ci/src/lib/cli/commands/merge-diffs.ts @@ -8,7 +8,7 @@ import type { CommandContext } from '../context.js'; export async function runMergeDiffs( files: string[], - { bin, config, directory, observer }: CommandContext, + { bin, config, directory }: CommandContext, ): Promise { const outputDir = path.join(directory, DEFAULT_PERSIST_OUTPUT_DIR); const filename = `merged-${DEFAULT_PERSIST_FILENAME}`; @@ -23,7 +23,6 @@ export async function runMergeDiffs( `--persist.filename=${filename}`, ], cwd: directory, - observer, }); return path.join(outputDir, `${filename}-diff.md`); diff --git a/packages/ci/src/lib/cli/commands/print-config.ts b/packages/ci/src/lib/cli/commands/print-config.ts index 6d740ff6f..24c3170a6 100644 --- a/packages/ci/src/lib/cli/commands/print-config.ts +++ b/packages/ci/src/lib/cli/commands/print-config.ts @@ -12,7 +12,6 @@ export async function runPrintConfig({ config, directory, project, - observer, }: CommandContext): Promise { // unique file name per project so command can be run in parallel const outputFile = ['code-pushup', 'config', project, 'json'] @@ -33,7 +32,6 @@ export async function runPrintConfig({ `--output=${outputPath}`, ], cwd: directory, - observer, }); try { diff --git a/packages/ci/src/lib/cli/context.ts b/packages/ci/src/lib/cli/context.ts index 1330bea05..9f93b89b3 100644 --- a/packages/ci/src/lib/cli/context.ts +++ b/packages/ci/src/lib/cli/context.ts @@ -1,15 +1,12 @@ -import type { ProcessObserver } from '@code-pushup/utils'; -import { createExecutionObserver } from '../create-execution-observer.js'; import type { Settings } from '../models.js'; import type { ProjectConfig } from '../monorepo/index.js'; export type CommandContext = Pick & { project?: string; - observer?: ProcessObserver; }; export function createCommandContext( - { config, bin, directory, silent }: Settings, + { config, bin, directory }: Settings, project: ProjectConfig | null | undefined, ): CommandContext { return { @@ -17,6 +14,5 @@ export function createCommandContext( directory: project?.directory ?? directory, config, ...(project?.name && { project: project.name }), - observer: createExecutionObserver({ silent }), }; } diff --git a/packages/ci/src/lib/cli/context.unit.test.ts b/packages/ci/src/lib/cli/context.unit.test.ts index 84b3b7b43..a56e09f73 100644 --- a/packages/ci/src/lib/cli/context.unit.test.ts +++ b/packages/ci/src/lib/cli/context.unit.test.ts @@ -2,29 +2,22 @@ import { expect } from 'vitest'; import { type CommandContext, createCommandContext } from './context.js'; describe('createCommandContext', () => { - const expectedObserver = expect.objectContaining({ - onStderr: expect.any(Function), - onStdout: expect.any(Function), - }); - it('should pick CLI-related settings in standalone mode', () => { expect( createCommandContext( { bin: 'npx --no-install code-pushup', config: null, - debug: false, detectNewIssues: true, directory: '/test', - logger: console, monorepo: false, parallel: false, nxProjectsFilter: '--with-target={task}', projects: null, - silent: false, task: 'code-pushup', skipComment: false, configPatterns: null, + searchCommits: false, }, null, ), @@ -32,7 +25,6 @@ describe('createCommandContext', () => { bin: 'npx --no-install code-pushup', directory: '/test', config: null, - observer: expectedObserver, }); }); @@ -42,18 +34,16 @@ describe('createCommandContext', () => { { bin: 'npx --no-install code-pushup', config: null, - debug: false, detectNewIssues: true, directory: '/test', - logger: console, monorepo: false, parallel: false, nxProjectsFilter: '--with-target={task}', projects: null, - silent: false, task: 'code-pushup', skipComment: false, configPatterns: null, + searchCommits: false, }, { name: 'ui', @@ -66,7 +56,6 @@ describe('createCommandContext', () => { directory: '/test/ui', config: null, project: 'ui', - observer: expectedObserver, }); }); }); diff --git a/packages/ci/src/lib/create-execution-observer.int.test.ts b/packages/ci/src/lib/create-execution-observer.int.test.ts deleted file mode 100644 index 547cceff9..000000000 --- a/packages/ci/src/lib/create-execution-observer.int.test.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { expect } from 'vitest'; -import { executeProcess } from '@code-pushup/utils'; -import { createExecutionObserver } from './create-execution-observer.js'; - -describe('createExecutionObserver', () => { - const message = 'This is stdout'; - const error = 'This is stderr'; - - it('should use execute process and use observer to capture stdout message and stderr will be empty', async () => { - const { stdout, stderr } = await executeProcess({ - command: 'node', - args: ['-e', `"console.log('${message}');"`], - observer: createExecutionObserver(), - }); - - expect(stdout).toMatch(message); - expect(stderr).toMatch(''); - }); - - it('should use execute process and use observer to capture stdout message and stderr will be error', async () => { - const { stdout, stderr } = await executeProcess({ - command: 'node', - args: ['-e', `"console.log('${message}'); console.error('${error}');"`], - observer: createExecutionObserver(), - }); - - expect(stdout).toMatch(message); - expect(stderr).toMatch(error); - }); - - it('should use execute process and use observer to capture stderr error and ignore stdout message', async () => { - const { stdout, stderr } = await executeProcess({ - command: 'node', - args: ['-e', `"console.log('${message}'); console.error('${error}');"`], - observer: createExecutionObserver({ silent: true }), - }); - - expect(stdout).toMatch(''); - expect(stderr).toMatch(error); - }); -}); diff --git a/packages/ci/src/lib/create-execution-observer.ts b/packages/ci/src/lib/create-execution-observer.ts deleted file mode 100644 index 9d555077c..000000000 --- a/packages/ci/src/lib/create-execution-observer.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { type ProcessObserver, logger } from '@code-pushup/utils'; - -export function createExecutionObserver( - { - silent, - }: { - silent?: boolean; - } = { silent: false }, -): ProcessObserver { - return { - onStderr: stderr => { - logger.warn(stderr); - }, - ...((!silent || logger.isVerbose()) && { - onStdout: stdout => { - logger.info(stdout); - }, - }), - }; -} diff --git a/packages/ci/src/lib/create-execution-observer.unit.test.ts b/packages/ci/src/lib/create-execution-observer.unit.test.ts deleted file mode 100644 index 86ccb944a..000000000 --- a/packages/ci/src/lib/create-execution-observer.unit.test.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { expect, vi } from 'vitest'; -import { createExecutionObserver } from './create-execution-observer.js'; - -describe('createExecutionObserver', () => { - it('should create execution observer with default settings', () => { - expect(createExecutionObserver()).toStrictEqual({ - onStderr: expect.any(Function), - onStdout: expect.any(Function), - }); - }); - - it('should create execution observer with silent false settings', () => { - expect(createExecutionObserver({ silent: false })).toStrictEqual({ - onStderr: expect.any(Function), - onStdout: expect.any(Function), - }); - }); - - it('should create execution observer with default silent taking priority over CP_VERBOSE flag', () => { - vi.stubEnv('CP_VERBOSE', 'false'); - - expect(createExecutionObserver()).toStrictEqual({ - onStderr: expect.any(Function), - onStdout: expect.any(Function), - }); - }); - - it('should create execution observer with silent setting', () => { - expect(createExecutionObserver({ silent: true })).toStrictEqual({ - onStderr: expect.any(Function), - }); - }); -}); diff --git a/packages/ci/src/lib/models.ts b/packages/ci/src/lib/models.ts index 784fcc204..ae5fb4aec 100644 --- a/packages/ci/src/lib/models.ts +++ b/packages/ci/src/lib/models.ts @@ -15,8 +15,6 @@ export type Options = { bin?: string; config?: string | null; directory?: string; - silent?: boolean; - debug?: boolean; detectNewIssues?: boolean; skipComment?: boolean; configPatterns?: ConfigPatterns | null; diff --git a/packages/ci/src/lib/monorepo/handlers/nx.ts b/packages/ci/src/lib/monorepo/handlers/nx.ts index c11b4f596..bcaec1e44 100644 --- a/packages/ci/src/lib/monorepo/handlers/nx.ts +++ b/packages/ci/src/lib/monorepo/handlers/nx.ts @@ -19,14 +19,13 @@ export const nxHandler: MonorepoToolHandler = { command: 'npx', args: ['nx', 'report'], cwd: options.cwd, - observer: options.observer, ignoreExitCode: true, }) ).code === 0 ); }, - async listProjects({ cwd, task, nxProjectsFilter, observer }) { + async listProjects({ cwd, task, nxProjectsFilter }) { const { stdout } = await executeProcess({ command: 'npx', args: [ @@ -37,7 +36,6 @@ export const nxHandler: MonorepoToolHandler = { '--json', ], cwd, - observer, }); const projects = parseProjects(stdout); return projects.toSorted().map(project => ({ diff --git a/packages/ci/src/lib/monorepo/list-projects.ts b/packages/ci/src/lib/monorepo/list-projects.ts index c127daa89..4b3b805e3 100644 --- a/packages/ci/src/lib/monorepo/list-projects.ts +++ b/packages/ci/src/lib/monorepo/list-projects.ts @@ -1,7 +1,6 @@ import { glob } from 'glob'; import path from 'node:path'; import { logger } from '@code-pushup/utils'; -import { createExecutionObserver } from '../create-execution-observer.js'; import type { Settings } from '../models.js'; import { detectMonorepoTool } from './detect-tool.js'; import { getToolHandler } from './handlers/index.js'; @@ -90,14 +89,12 @@ function createMonorepoHandlerOptions({ directory, parallel, nxProjectsFilter, - silent, }: Settings): MonorepoHandlerOptions { return { task, cwd: directory, parallel, nxProjectsFilter, - observer: createExecutionObserver({ silent }), }; } diff --git a/packages/ci/src/lib/monorepo/list-projects.unit.test.ts b/packages/ci/src/lib/monorepo/list-projects.unit.test.ts index 8518e8f04..6adbee150 100644 --- a/packages/ci/src/lib/monorepo/list-projects.unit.test.ts +++ b/packages/ci/src/lib/monorepo/list-projects.unit.test.ts @@ -64,7 +64,6 @@ describe('listMonorepoProjects', () => { command: 'npx', args: ['nx', 'show', 'projects', '--with-target=code-pushup', '--json'], cwd: process.cwd(), - observer: expect.any(Object), }); }); diff --git a/packages/ci/src/lib/monorepo/tools.ts b/packages/ci/src/lib/monorepo/tools.ts index ef5554bbc..4a1256798 100644 --- a/packages/ci/src/lib/monorepo/tools.ts +++ b/packages/ci/src/lib/monorepo/tools.ts @@ -1,5 +1,3 @@ -import type { ProcessObserver } from '@code-pushup/utils'; - export const MONOREPO_TOOLS = ['nx', 'turbo', 'yarn', 'pnpm', 'npm'] as const; export type MonorepoTool = (typeof MONOREPO_TOOLS)[number]; @@ -17,7 +15,6 @@ export type MonorepoHandlerOptions = { task: string; cwd: string; parallel: boolean | number; - observer?: ProcessObserver; nxProjectsFilter: string | string[]; }; diff --git a/packages/ci/src/lib/run-utils.ts b/packages/ci/src/lib/run-utils.ts index 609cf89f0..000ca1883 100644 --- a/packages/ci/src/lib/run-utils.ts +++ b/packages/ci/src/lib/run-utils.ts @@ -90,8 +90,6 @@ export async function createRunEnv( options: Options | undefined, git: SimpleGit, ): Promise { - logger.setVerbose(options?.debug === true || options?.silent === false); - const [head, base] = await Promise.all([ normalizeGitRef(refs.head, git), refs.base && normalizeGitRef(refs.base, git), diff --git a/packages/ci/src/lib/run.int.test.ts b/packages/ci/src/lib/run.int.test.ts index f3a7cc8f9..0a43e01ba 100644 --- a/packages/ci/src/lib/run.int.test.ts +++ b/packages/ci/src/lib/run.int.test.ts @@ -101,11 +101,6 @@ describe('runInCI', () => { url: 'https://fake.hosted.git/comments/42', }; - const expectedObserver = expect.objectContaining({ - onStderr: expect.any(Function), - onStdout: expect.any(Function), - }); - let includeUploadConfig: boolean; let git: SimpleGit; @@ -286,13 +281,11 @@ describe('runInCI', () => { command: options.bin, args: ['print-config', expect.stringMatching(/^--output=.*\.json$/)], cwd: workDir, - observer: expectedObserver, } satisfies utils.ProcessConfig); expect(utils.executeProcess).toHaveBeenNthCalledWith(2, { command: options.bin, args: [], cwd: workDir, - observer: expectedObserver, } satisfies utils.ProcessConfig); expect(logger.error).not.toHaveBeenCalled(); @@ -364,31 +357,26 @@ describe('runInCI', () => { command: options.bin, args: ['print-config', expect.stringMatching(/^--output=.*\.json$/)], cwd: workDir, - observer: expectedObserver, } satisfies utils.ProcessConfig); expect(utils.executeProcess).toHaveBeenNthCalledWith(2, { command: options.bin, args: [], cwd: workDir, - observer: expectedObserver, } satisfies utils.ProcessConfig); expect(utils.executeProcess).toHaveBeenNthCalledWith(3, { command: options.bin, args: ['print-config', expect.stringMatching(/^--output=.*\.json$/)], cwd: workDir, - observer: expectedObserver, } satisfies utils.ProcessConfig); expect(utils.executeProcess).toHaveBeenNthCalledWith(4, { command: options.bin, args: [], cwd: workDir, - observer: expectedObserver, } satisfies utils.ProcessConfig); expect(utils.executeProcess).toHaveBeenNthCalledWith(5, { command: options.bin, args: ['compare'], cwd: workDir, - observer: expectedObserver, } satisfies utils.ProcessConfig); expect(logger.error).not.toHaveBeenCalled(); @@ -442,19 +430,16 @@ describe('runInCI', () => { command: options.bin, args: ['print-config', expect.stringMatching(/^--output=.*\.json$/)], cwd: workDir, - observer: expectedObserver, } satisfies utils.ProcessConfig); expect(utils.executeProcess).toHaveBeenNthCalledWith(2, { command: options.bin, args: [], cwd: workDir, - observer: expectedObserver, } satisfies utils.ProcessConfig); expect(utils.executeProcess).toHaveBeenNthCalledWith(3, { command: options.bin, args: ['compare'], cwd: workDir, - observer: expectedObserver, } satisfies utils.ProcessConfig); expect(logger.error).not.toHaveBeenCalled(); @@ -513,19 +498,16 @@ describe('runInCI', () => { command: options.bin, args: ['print-config', expect.stringMatching(/^--output=.*\.json$/)], cwd: workDir, - observer: expectedObserver, } satisfies utils.ProcessConfig); expect(utils.executeProcess).toHaveBeenNthCalledWith(2, { command: options.bin, args: [], cwd: workDir, - observer: expectedObserver, } satisfies utils.ProcessConfig); expect(utils.executeProcess).toHaveBeenNthCalledWith(3, { command: options.bin, args: ['compare'], cwd: workDir, - observer: expectedObserver, } satisfies utils.ProcessConfig); expect(logger.error).not.toHaveBeenCalled(); @@ -572,7 +554,6 @@ describe('runInCI', () => { command: options.bin, args: expect.arrayContaining(['compare']), cwd: workDir, - observer: expectedObserver, } satisfies utils.ProcessConfig); }); }); @@ -706,13 +687,11 @@ describe('runInCI', () => { expect.stringMatching(/^--output=.*\.json$/), ], cwd: expect.stringContaining(workDir), - observer: expectedObserver, } satisfies utils.ProcessConfig); expect(utils.executeProcess).toHaveBeenCalledWith({ command: runMany, args: [], cwd: expect.stringContaining(workDir), - observer: expectedObserver, } satisfies utils.ProcessConfig); expect(logger.error).not.toHaveBeenCalled(); @@ -782,7 +761,6 @@ describe('runInCI', () => { command: runMany, args: [], cwd: expect.stringContaining(workDir), - observer: expectedObserver, } satisfies utils.ProcessConfig); expect(logger.error).not.toHaveBeenCalled(); @@ -945,19 +923,16 @@ describe('runInCI', () => { expect.stringMatching(/^--output=.*\.json$/), ], cwd: expect.stringContaining(workDir), - observer: expectedObserver, } satisfies utils.ProcessConfig); expect(utils.executeProcess).toHaveBeenCalledWith({ command: runMany, args: [], cwd: expect.stringContaining(workDir), - observer: expectedObserver, } satisfies utils.ProcessConfig); expect(utils.executeProcess).toHaveBeenCalledWith({ command: runMany, args: ['compare'], cwd: expect.stringContaining(workDir), - observer: expectedObserver, } satisfies utils.ProcessConfig); expect(utils.executeProcess).toHaveBeenCalledWith({ command: run, @@ -976,7 +951,6 @@ describe('runInCI', () => { '--persist.filename=merged-report', ], cwd: expect.stringContaining(workDir), - observer: expectedObserver, } satisfies utils.ProcessConfig); expect(logger.error).not.toHaveBeenCalled(); @@ -1052,13 +1026,11 @@ describe('runInCI', () => { command: runMany, args: [], cwd: expect.stringContaining(workDir), - observer: expectedObserver, } satisfies utils.ProcessConfig); expect(utils.executeProcess).toHaveBeenCalledWith({ command: runMany, args: ['compare'], cwd: expect.stringContaining(workDir), - observer: expectedObserver, } satisfies utils.ProcessConfig); expect(utils.executeProcess).toHaveBeenCalledWith({ command: run, @@ -1077,7 +1049,6 @@ describe('runInCI', () => { '--persist.filename=merged-report', ], cwd: expect.stringContaining(workDir), - observer: expectedObserver, } satisfies utils.ProcessConfig); expect(utils.executeProcess).not.toHaveBeenCalledWith( expect.objectContaining({ @@ -1132,19 +1103,16 @@ describe('runInCI', () => { command: runMany, args: expect.any(Array), cwd: expect.stringContaining(workDir), - observer: expectedObserver, } satisfies utils.ProcessConfig); expect(utils.executeProcess).toHaveBeenCalledWith({ command: runMany, args: expect.arrayContaining(['compare']), cwd: expect.stringContaining(workDir), - observer: expectedObserver, } satisfies utils.ProcessConfig); expect(utils.executeProcess).toHaveBeenCalledWith({ command: run, args: expect.arrayContaining(['merge-diffs']), cwd: expect.stringContaining(workDir), - observer: expectedObserver, } satisfies utils.ProcessConfig); }); }); @@ -1236,13 +1204,11 @@ describe('runInCI', () => { command: options.bin, args: ['print-config', expect.stringMatching(/^--output=.*\.json$/)], cwd: expect.stringContaining(workDir), - observer: expectedObserver, } satisfies utils.ProcessConfig); expect(utils.executeProcess).toHaveBeenCalledWith({ command: options.bin, args: [], cwd: expect.stringContaining(workDir), - observer: expectedObserver, } satisfies utils.ProcessConfig); expect(logger.error).not.toHaveBeenCalled(); @@ -1405,19 +1371,16 @@ describe('runInCI', () => { command: options.bin, args: ['print-config', expect.stringMatching(/^--output=.*\.json$/)], cwd: expect.stringContaining(workDir), - observer: expectedObserver, } satisfies utils.ProcessConfig); expect(utils.executeProcess).toHaveBeenCalledWith({ command: options.bin, args: [], cwd: expect.stringContaining(workDir), - observer: expectedObserver, } satisfies utils.ProcessConfig); expect(utils.executeProcess).toHaveBeenCalledWith({ command: options.bin, args: ['compare'], cwd: expect.stringContaining(workDir), - observer: expectedObserver, } satisfies utils.ProcessConfig); expect(utils.executeProcess).toHaveBeenCalledWith({ command: options.bin, @@ -1436,7 +1399,6 @@ describe('runInCI', () => { '--persist.filename=merged-report', ], cwd: expect.stringContaining(workDir), - observer: expectedObserver, } satisfies utils.ProcessConfig); expect(logger.error).not.toHaveBeenCalled(); diff --git a/packages/ci/src/lib/settings.ts b/packages/ci/src/lib/settings.ts index b16df6708..800b27bb9 100644 --- a/packages/ci/src/lib/settings.ts +++ b/packages/ci/src/lib/settings.ts @@ -10,8 +10,6 @@ export const DEFAULT_SETTINGS: Settings = { bin: 'npx --no-install code-pushup', config: null, directory: process.cwd(), - silent: false, - debug: false, detectNewIssues: true, nxProjectsFilter: '--with-target={task}', skipComment: false, diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index f223a1f06..4c873dbea 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -9,7 +9,7 @@ export { } from './lib/compare.js'; export { getRunnerOutputsPath, - type ValidatedRunnerResult, + type RunnerResult, } from './lib/implementation/runner.js'; export { @@ -23,7 +23,6 @@ export { executePlugin, executePlugins, } from './lib/implementation/execute-plugin.js'; -export { AuditOutputsMissingAuditError } from './lib/implementation/runner.js'; export { PersistDirError, PersistError, @@ -34,6 +33,7 @@ export { ConfigPathError, readRcByPath, } from './lib/implementation/read-rc-file.js'; +export { AuditOutputsMissingAuditError } from './lib/implementation/runner.js'; export { mergeDiffs } from './lib/merge-diffs.js'; export type { GlobalOptions } from './lib/types.js'; export { upload, type UploadOptions } from './lib/upload.js'; diff --git a/packages/core/src/lib/implementation/runner.ts b/packages/core/src/lib/implementation/runner.ts index 0db6becd0..7bf9fb650 100644 --- a/packages/core/src/lib/implementation/runner.ts +++ b/packages/core/src/lib/implementation/runner.ts @@ -14,41 +14,27 @@ import { ensureDirectoryExists, executeProcess, fileExists, - isVerbose, readJsonFile, removeDirectoryIfExists, runnerArgsToEnv, - ui, } from '@code-pushup/utils'; import { normalizeAuditOutputs } from '../normalize.js'; export type RunnerResult = { date: string; duration: number; - audits: unknown; -}; - -export type ValidatedRunnerResult = Omit & { audits: AuditOutputs; }; export async function executeRunnerConfig( config: RunnerConfig, args: RunnerArgs, -): Promise { +): Promise { const { outputFile, outputTransform } = config; - const { duration, date } = await executeProcess({ + await executeProcess({ command: config.command, args: config.args, - observer: { - onStdout: stdout => { - if (isVerbose()) { - ui().logger.log(stdout); - } - }, - onStderr: stderr => ui().logger.error(stderr), - }, env: { ...process.env, ...runnerArgsToEnv(args) }, }); @@ -60,30 +46,16 @@ export async function executeRunnerConfig( // transform unknownAuditOutputs to auditOutputs const audits = outputTransform ? await outputTransform(outputs) : outputs; - // create runner result - return { - duration, - date, - audits, - }; + return audits; } export async function executeRunnerFunction( runner: RunnerFunction, args: RunnerArgs, -): Promise { - const date = new Date().toISOString(); - const start = performance.now(); - +): Promise { // execute plugin runner const audits = await runner(args); - - // create runner result - return { - date, - duration: calcDuration(start), - audits, - }; + return audits; } /** @@ -102,13 +74,18 @@ export class AuditOutputsMissingAuditError extends Error { export async function executePluginRunner( pluginConfig: Pick, args: RunnerArgs, -): Promise & { audits: AuditOutputs }> { +): Promise { const { audits: pluginConfigAudits, runner } = pluginConfig; - const runnerResult: RunnerResult = + + const date = new Date().toISOString(); + const start = performance.now(); + + const unvalidatedAuditOutputs = typeof runner === 'object' ? await executeRunnerConfig(runner, args) : await executeRunnerFunction(runner, args); - const { audits: unvalidatedAuditOutputs, ...executionMeta } = runnerResult; + + const duration = calcDuration(start); const result = auditOutputsSchema.safeParse(unvalidatedAuditOutputs); if (!result.success) { @@ -118,7 +95,8 @@ export async function executePluginRunner( auditOutputsCorrelateWithPluginOutput(auditOutputs, pluginConfigAudits); return { - ...executionMeta, + date, + duration, audits: await normalizeAuditOutputs(auditOutputs), }; } @@ -150,7 +128,7 @@ export function getRunnerOutputsPath(pluginSlug: string, outputDir: string) { export async function writeRunnerResults( pluginSlug: string, outputDir: string, - runnerResult: ValidatedRunnerResult, + runnerResult: RunnerResult, ): Promise { const cacheFilePath = getRunnerOutputsPath(pluginSlug, outputDir); await ensureDirectoryExists(path.dirname(cacheFilePath)); @@ -160,7 +138,7 @@ export async function writeRunnerResults( export async function readRunnerResults( pluginSlug: string, outputDir: string, -): Promise { +): Promise { const auditOutputsPath = getRunnerOutputsPath(pluginSlug, outputDir); if (await fileExists(auditOutputsPath)) { const cachedResult = await readJsonFile(auditOutputsPath); diff --git a/packages/core/src/lib/implementation/runner.unit.test.ts b/packages/core/src/lib/implementation/runner.unit.test.ts index 5f4bd8cf8..57770a279 100644 --- a/packages/core/src/lib/implementation/runner.unit.test.ts +++ b/packages/core/src/lib/implementation/runner.unit.test.ts @@ -9,7 +9,6 @@ import { auditOutputsSchema, } from '@code-pushup/models'; import { - ISO_STRING_REGEXP, MEMFS_VOLUME, MINIMAL_PLUGIN_CONFIG_MOCK, MINIMAL_RUNNER_CONFIG_MOCK, @@ -18,7 +17,6 @@ import { } from '@code-pushup/test-utils'; import * as utils from '@code-pushup/utils'; import { - type RunnerResult, executePluginRunner, executeRunnerConfig, executeRunnerFunction, @@ -52,17 +50,18 @@ describe('executeRunnerConfig', () => { }); it('should execute valid runner config', async () => { - const runnerResult = await executeRunnerConfig(MINIMAL_RUNNER_CONFIG_MOCK, { - persist: DEFAULT_PERSIST_CONFIG, - }); + const auditOutputs = (await executeRunnerConfig( + MINIMAL_RUNNER_CONFIG_MOCK, + { + persist: DEFAULT_PERSIST_CONFIG, + }, + )) as AuditOutputs; // data sanity - expect((runnerResult.audits as AuditOutputs)[0]?.slug).toBe('node-version'); - expect(runnerResult.date).toMatch(ISO_STRING_REGEXP); - expect(runnerResult.duration).toBeGreaterThanOrEqual(0); + expect(auditOutputs[0]?.slug).toBe('node-version'); // schema validation - expect(() => auditOutputsSchema.parse(runnerResult.audits)).not.toThrow(); + expect(() => auditOutputsSchema.parse(auditOutputs)).not.toThrow(); // executed process configuration expect(utils.executeProcess).toHaveBeenCalledWith<[utils.ProcessConfig]>({ @@ -74,15 +73,11 @@ describe('executeRunnerConfig', () => { CP_PERSIST_FORMAT: DEFAULT_PERSIST_FORMAT.join(','), CP_PERSIST_SKIP_REPORTS: `${DEFAULT_PERSIST_SKIP_REPORT}`, }), - observer: { - onStdout: expect.any(Function), - onStderr: expect.any(Function), - }, }); }); it('should use outputTransform when provided', async () => { - const runnerResult = await executeRunnerConfig( + const auditOutputs = (await executeRunnerConfig( { command: 'node', args: ['-v'], @@ -98,8 +93,7 @@ describe('executeRunnerConfig', () => { ]), }, { persist: DEFAULT_PERSIST_CONFIG }, - ); - const auditOutputs = runnerResult.audits as AuditOutputs; + )) as AuditOutputs; expect(auditOutputs[0]?.slug).toBe('node-version'); expect(auditOutputs[0]?.displayValue).toBe('16.0.0'); @@ -123,11 +117,10 @@ describe('executeRunnerConfig', () => { describe('executeRunnerFunction', () => { it('should execute a valid runner function', async () => { - const runnerResult: RunnerResult = await executeRunnerFunction( + const auditOutputs = (await executeRunnerFunction( MINIMAL_RUNNER_FUNCTION_MOCK, { persist: DEFAULT_PERSIST_CONFIG }, - ); - const auditOutputs = runnerResult.audits as AuditOutputs; + )) as AuditOutputs; expect(auditOutputs[0]?.slug).toBe('node-version'); expect(auditOutputs[0]?.details?.issues).toEqual([ diff --git a/packages/create-cli/src/lib/init.ts b/packages/create-cli/src/lib/init.ts index 5aba7c9bc..77b1e81cb 100644 --- a/packages/create-cli/src/lib/init.ts +++ b/packages/create-cli/src/lib/init.ts @@ -29,14 +29,6 @@ export async function initCodePushup() { ...nxPluginGenerator('init', { skipNxJson: true, }), - observer: { - onStdout: data => { - console.info(parseNxProcessOutput(data.toString())); - }, - onError: error => { - console.error(parseNxProcessOutput(error.message.toString())); - }, - }, }); const { stdout: configStdout, stderr: configStderr } = await executeProcess( diff --git a/packages/create-cli/src/lib/init.unit.test.ts b/packages/create-cli/src/lib/init.unit.test.ts index e231b0dfb..2ad9a52fc 100644 --- a/packages/create-cli/src/lib/init.unit.test.ts +++ b/packages/create-cli/src/lib/init.unit.test.ts @@ -59,7 +59,6 @@ describe('initCodePushup', () => { expect(spyExecuteProcess).toHaveBeenNthCalledWith(1, { command: 'npx', args: ['nx', 'g', '@code-pushup/nx-plugin:init', '--skipNxJson'], - observer: expect.any(Object), }); expect(spyParseNxProcessOutput).toHaveBeenNthCalledWith(1, 'stdout-mock'); expect(spyExecuteProcess).toHaveBeenNthCalledWith(2, { diff --git a/packages/nx-plugin/package.json b/packages/nx-plugin/package.json index 9a5917a64..36da07f7f 100644 --- a/packages/nx-plugin/package.json +++ b/packages/nx-plugin/package.json @@ -35,7 +35,6 @@ "@code-pushup/models": "0.84.0", "@code-pushup/utils": "0.84.0", "@nx/devkit": ">=17.0.0", - "ansis": "^3.3.0", "nx": ">=17.0.0" }, "files": [ diff --git a/packages/nx-plugin/src/executors/cli/executor.int.test.ts b/packages/nx-plugin/src/executors/cli/executor.int.test.ts index 740486f33..fee1e59d8 100644 --- a/packages/nx-plugin/src/executors/cli/executor.int.test.ts +++ b/packages/nx-plugin/src/executors/cli/executor.int.test.ts @@ -13,11 +13,11 @@ describe('runAutorunExecutor', () => { beforeEach(() => { executeProcessSpy.mockResolvedValue({ + bin: 'npx ...', code: 0, + signal: null, stdout: '', stderr: '', - date: new Date().toISOString(), - duration: 100, }); }); @@ -47,10 +47,6 @@ describe('runAutorunExecutor', () => { command: 'npx', args: expect.arrayContaining(['@code-pushup/cli']), cwd: process.cwd(), - observer: { - onError: expect.any(Function), - onStdout: expect.any(Function), - }, }); }); }); diff --git a/packages/nx-plugin/src/executors/cli/executor.unit.test.ts b/packages/nx-plugin/src/executors/cli/executor.unit.test.ts index acad51ac5..c5684b0ba 100644 --- a/packages/nx-plugin/src/executors/cli/executor.unit.test.ts +++ b/packages/nx-plugin/src/executors/cli/executor.unit.test.ts @@ -26,11 +26,11 @@ describe('runAutorunExecutor', () => { beforeEach(() => { vi.unstubAllEnvs(); executeProcessSpy.mockResolvedValue({ + bin: 'npx ...', code: 0, + signal: null, stdout: '', stderr: '', - date: new Date().toISOString(), - duration: 100, }); }); @@ -48,10 +48,6 @@ describe('runAutorunExecutor', () => { command: 'npx', args: expect.arrayContaining(['@code-pushup/cli']), cwd: MEMFS_VOLUME, - observer: { - onError: expect.any(Function), - onStdout: expect.any(Function), - }, }); }); @@ -69,10 +65,6 @@ describe('runAutorunExecutor', () => { command: 'npx', args: expect.arrayContaining(['@code-pushup/cli']), cwd: 'cwd-form-context', - observer: { - onError: expect.any(Function), - onStdout: expect.any(Function), - }, }); }); diff --git a/packages/nx-plugin/src/executors/internal/cli.ts b/packages/nx-plugin/src/executors/internal/cli.ts index bab74a8f1..adbf1627c 100644 --- a/packages/nx-plugin/src/executors/internal/cli.ts +++ b/packages/nx-plugin/src/executors/internal/cli.ts @@ -1,5 +1,4 @@ import { logger } from '@nx/devkit'; -import type { ProcessConfig } from '../../internal/execute-process.js'; export function createCliCommandString(options?: { args?: Record; @@ -16,19 +15,11 @@ export function createCliCommandObject(options?: { args?: Record; command?: string; bin?: string; -}): ProcessConfig { +}): import('@code-pushup/utils').ProcessConfig { const { bin = '@code-pushup/cli', command, args } = options ?? {}; return { command: 'npx', args: [bin, ...objectToCliArgs({ _: command ?? [], ...args })], - observer: { - onError: error => { - logger.error(error.message); - }, - onStdout: data => { - logger.log(data); - }, - }, }; } diff --git a/packages/nx-plugin/src/executors/internal/cli.unit.test.ts b/packages/nx-plugin/src/executors/internal/cli.unit.test.ts index b8251484c..279f23505 100644 --- a/packages/nx-plugin/src/executors/internal/cli.unit.test.ts +++ b/packages/nx-plugin/src/executors/internal/cli.unit.test.ts @@ -109,10 +109,6 @@ describe('createCliCommandObject', () => { expect(createCliCommandObject({ args: { verbose: true } })).toStrictEqual({ args: ['@code-pushup/cli', '--verbose'], command: 'npx', - observer: { - onError: expect.any(Function), - onStdout: expect.any(Function), - }, }); }); @@ -124,10 +120,6 @@ describe('createCliCommandObject', () => { ).toStrictEqual({ args: ['@code-pushup/cli', 'autorun', '--verbose'], command: 'npx', - observer: { - onError: expect.any(Function), - onStdout: expect.any(Function), - }, }); }); @@ -139,10 +131,6 @@ describe('createCliCommandObject', () => { ).toStrictEqual({ args: ['node_modules/@code-pushup/cli/src/bin.js'], command: 'npx', - observer: { - onError: expect.any(Function), - onStdout: expect.any(Function), - }, }); }); }); diff --git a/packages/nx-plugin/src/index.ts b/packages/nx-plugin/src/index.ts index 356be758c..c074211c6 100644 --- a/packages/nx-plugin/src/index.ts +++ b/packages/nx-plugin/src/index.ts @@ -17,9 +17,5 @@ export { configurationGenerator } from './generators/configuration/generator.js' export type { ConfigurationGeneratorOptions } from './generators/configuration/schema.js'; export { initGenerator, initSchematic } from './generators/init/generator.js'; export { type InitGeneratorSchema } from './generators/init/schema.js'; -export { - executeProcess, - type ProcessConfig, -} from './internal/execute-process.js'; export * from './internal/versions.js'; export { createNodes, createNodesV2 } from './plugin/index.js'; diff --git a/packages/nx-plugin/src/internal/execute-process.ts b/packages/nx-plugin/src/internal/execute-process.ts index cf61f3e84..9f8f68a05 100644 --- a/packages/nx-plugin/src/internal/execute-process.ts +++ b/packages/nx-plugin/src/internal/execute-process.ts @@ -1,186 +1,11 @@ -import { gray } from 'ansis'; -import { spawn } from 'node:child_process'; -import { ui } from '@code-pushup/utils'; - -export function calcDuration(start: number, stop?: number): number { - return Math.round((stop ?? performance.now()) - start); -} - -/** - * Represents the process result. - * @category Types - * @public - * @property {string} stdout - The stdout of the process. - * @property {string} stderr - The stderr of the process. - * @property {number | null} code - The exit code of the process. - */ -export type ProcessResult = { - stdout: string; - stderr: string; - code: number | null; - date: string; - duration: number; -}; - -/** - * Error class for process errors. - * Contains additional information about the process result. - * @category Error - * @public - * @class - * @extends Error - * @example - * const result = await executeProcess({}) - * .catch((error) => { - * if (error instanceof ProcessError) { - * console.error(error.code); - * console.error(error.stderr); - * console.error(error.stdout); - * } - * }); - * - */ -export class ProcessError extends Error { - code: number | null; - stderr: string; - stdout: string; - - constructor(result: ProcessResult) { - super(result.stderr); - this.code = result.code; - this.stderr = result.stderr; - this.stdout = result.stdout; - } -} - -/** - * Process config object. Contains the command, args and observer. - * @param cfg - process config object with command, args and observer (optional) - * @category Types - * @public - * @property {string} command - The command to execute. - * @property {string[]} args - The arguments for the command. - * @property {ProcessObserver} observer - The observer for the process. - * - * @example - * - * // bash command - * const cfg = { - * command: 'bash', - * args: ['-c', 'echo "hello world"'] - * }; - * - * // node command - * const cfg = { - * command: 'node', - * args: ['--version'] - * }; - * - * // npx command - * const cfg = { - * command: 'npx', - * args: ['--version'] - * - */ -export type ProcessConfig = { - command: string; - args?: string[]; - cwd?: string; - observer?: ProcessObserver; - ignoreExitCode?: boolean; -}; - -/** - * Process observer object. Contains the onStdout, error and complete function. - * @category Types - * @public - * @property {function} onStdout - The onStdout function of the observer (optional). - * @property {function} onError - The error function of the observer (optional). - * @property {function} onComplete - The complete function of the observer (optional). - * - * @example - * const observer = { - * onStdout: (stdout) => console.info(stdout) - * } - */ -export type ProcessObserver = { - onStdout?: (stdout: string) => void; - onError?: (error: ProcessError) => void; - onComplete?: () => void; -}; - /** - * Executes a process and returns a promise with the result as `ProcessResult`. - * - * @example - * - * // sync process execution - * const result = await executeProcess({ - * command: 'node', - * args: ['--version'] - * }); - * - * console.info(result); - * - * // async process execution - * const result = await executeProcess({ - * command: 'node', - * args: ['download-data'], - * observer: { - * onStdout: updateProgress, - * error: handleError, - * complete: cleanLogs, - * } - * }); - * - * console.info(result); + * Dynamically imports and executes function from utils. * - * @param cfg - see {@link ProcessConfig} + * This is a workaround for Nx only supporting plugins in CommonJS format. */ -export function executeProcess(cfg: ProcessConfig): Promise { - const { observer, cwd, command, args, ignoreExitCode = false } = cfg; - const { onStdout, onError, onComplete } = observer ?? {}; - const date = new Date().toISOString(); - const start = performance.now(); - - const logCommand = [command, ...(args || [])].join(' '); - ui().logger.log( - gray( - `Executing command:\n${logCommand}\nIn working directory:\n${cfg.cwd ?? process.cwd()}`, - ), - ); - - return new Promise((resolve, reject) => { - // shell:true tells Windows to use shell command for spawning a child process - const process = spawn(command, args, { cwd, shell: true }); - // eslint-disable-next-line functional/no-let - let stdout = ''; - // eslint-disable-next-line functional/no-let - let stderr = ''; - - process.stdout.on('data', data => { - stdout += String(data); - onStdout?.(String(data)); - }); - - process.stderr.on('data', data => { - stderr += String(data); - }); - - process.on('error', err => { - stderr += err.toString(); - }); - - process.on('close', code => { - const timings = { date, duration: calcDuration(start) }; - if (code === 0 || ignoreExitCode) { - onComplete?.(); - resolve({ code, stdout, stderr, ...timings }); - } else { - const errorMsg = new ProcessError({ code, stdout, stderr, ...timings }); - onError?.(errorMsg); - reject(errorMsg); - } - }); - }); +export async function executeProcess( + cfg: import('@code-pushup/utils').ProcessConfig, +): Promise { + const { executeProcess } = await import('@code-pushup/utils'); + return executeProcess(cfg); } diff --git a/packages/nx-plugin/src/internal/execute-process.unit.test.ts b/packages/nx-plugin/src/internal/execute-process.unit.test.ts deleted file mode 100644 index 5893b867f..000000000 --- a/packages/nx-plugin/src/internal/execute-process.unit.test.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { describe, expect, it, vi } from 'vitest'; -import { getAsyncProcessRunnerConfig } from '@code-pushup/test-utils'; -import { type ProcessObserver, executeProcess } from './execute-process.js'; - -describe('executeProcess', () => { - const spyObserver: ProcessObserver = { - onStdout: vi.fn(), - onError: vi.fn(), - onComplete: vi.fn(), - }; - const errorSpy = vi.fn(); - - beforeEach(() => { - vi.clearAllMocks(); - }); - - it('should work with node command `node -v`', async () => { - const processResult = await executeProcess({ - command: `node`, - args: ['-v'], - observer: spyObserver, - }); - - // Note: called once or twice depending on environment (2nd time for a new line) - expect(spyObserver.onStdout).toHaveBeenCalled(); - expect(spyObserver.onComplete).toHaveBeenCalledOnce(); - expect(spyObserver.onError).not.toHaveBeenCalled(); - expect(processResult.stdout).toMatch(/v\d{1,2}(\.\d{1,2}){0,2}/); - }); - - it('should work with npx command `npx --help`', async () => { - const processResult = await executeProcess({ - command: `npx`, - args: ['--help'], - observer: spyObserver, - }); - expect(spyObserver.onStdout).toHaveBeenCalledOnce(); - expect(spyObserver.onComplete).toHaveBeenCalledOnce(); - expect(spyObserver.onError).not.toHaveBeenCalled(); - expect(processResult.stdout).toContain('npm exec'); - }); - - it('should work with script `node custom-script.js`', async () => { - const processResult = await executeProcess({ - ...getAsyncProcessRunnerConfig({ interval: 10, runs: 4 }), - observer: spyObserver, - }).catch(errorSpy); - - expect(errorSpy).not.toHaveBeenCalled(); - expect(processResult.stdout).toContain('process:complete'); - expect(spyObserver.onStdout).toHaveBeenCalledTimes(6); // intro + 4 runs + complete - expect(spyObserver.onError).not.toHaveBeenCalled(); - expect(spyObserver.onComplete).toHaveBeenCalledOnce(); - }); - - it('should work with async script `node custom-script.js` that throws an error', async () => { - const processResult = await executeProcess({ - ...getAsyncProcessRunnerConfig({ - interval: 10, - runs: 1, - throwError: true, - }), - observer: spyObserver, - }).catch(errorSpy); - - expect(errorSpy).toHaveBeenCalledOnce(); - expect(processResult).toBeUndefined(); - expect(spyObserver.onStdout).toHaveBeenCalledTimes(2); // intro + 1 run before error - expect(spyObserver.onError).toHaveBeenCalledOnce(); - expect(spyObserver.onComplete).not.toHaveBeenCalled(); - }); - - it('should successfully exit process after an error is thrown when ignoreExitCode is set', async () => { - const processResult = await executeProcess({ - ...getAsyncProcessRunnerConfig({ - interval: 10, - runs: 1, - throwError: true, - }), - observer: spyObserver, - ignoreExitCode: true, - }).catch(errorSpy); - - expect(errorSpy).not.toHaveBeenCalled(); - expect(processResult.code).toBe(1); - expect(processResult.stdout).toContain('process:update'); - expect(processResult.stderr).toContain('dummy-error'); - expect(spyObserver.onStdout).toHaveBeenCalledTimes(2); // intro + 1 run before error - expect(spyObserver.onError).not.toHaveBeenCalled(); - expect(spyObserver.onComplete).toHaveBeenCalledOnce(); - }); -}); diff --git a/packages/plugin-js-packages/src/lib/runner/index.ts b/packages/plugin-js-packages/src/lib/runner/index.ts index 98fc1047f..a4921ff75 100644 --- a/packages/plugin-js-packages/src/lib/runner/index.ts +++ b/packages/plugin-js-packages/src/lib/runner/index.ts @@ -2,12 +2,11 @@ import { writeFile } from 'node:fs/promises'; import path from 'node:path'; import type { RunnerConfig, RunnerFilesPaths } from '@code-pushup/models'; import { + asyncSequential, createRunnerFiles, ensureDirectoryExists, executeProcess, filePathToCliArg, - isPromiseFulfilledResult, - isPromiseRejectedResult, objectFromEntries, objectToCliArgs, readJsonFile, @@ -116,34 +115,21 @@ async function processAudit( supportedAuditDepGroups.includes(group), ); - const auditResults = await Promise.allSettled( - compatibleAuditDepGroups.map( - async (depGroup): Promise<[DependencyGroup, AuditResult]> => { - const { stdout } = await executeProcess({ - command: pm.command, - args: pm.audit.getCommandArgs(depGroup), - cwd: packageJsonPath ? path.dirname(packageJsonPath) : process.cwd(), - ignoreExitCode: pm.audit.ignoreExitCode, - }); - return [depGroup, pm.audit.unifyResult(stdout)]; - }, - ), - ); - - const rejected = auditResults.filter(isPromiseRejectedResult); - if (rejected.length > 0) { - rejected.forEach(result => { - console.error(result.reason); - }); - - throw new Error(`JS Packages plugin: Running ${pm.name} audit failed.`); - } - - const fulfilled = objectFromEntries( - auditResults.filter(isPromiseFulfilledResult).map(x => x.value), + const auditResults = await asyncSequential( + compatibleAuditDepGroups, + async (depGroup): Promise<[DependencyGroup, AuditResult]> => { + const { stdout } = await executeProcess({ + command: pm.command, + args: pm.audit.getCommandArgs(depGroup), + cwd: packageJsonPath ? path.dirname(packageJsonPath) : process.cwd(), + ignoreExitCode: pm.audit.ignoreExitCode, + }); + return [depGroup, pm.audit.unifyResult(stdout)]; + }, ); - const uniqueResults = pm.audit.postProcessResult?.(fulfilled) ?? fulfilled; + const resultsMap = objectFromEntries(auditResults); + const uniqueResults = pm.audit.postProcessResult?.(resultsMap) ?? resultsMap; return compatibleAuditDepGroups.map(depGroup => auditResultToAuditOutput( diff --git a/packages/utils/mocks/logger-demo.ts b/packages/utils/mocks/logger-demo.ts index 741826625..a6020cde8 100644 --- a/packages/utils/mocks/logger-demo.ts +++ b/packages/utils/mocks/logger-demo.ts @@ -11,7 +11,11 @@ async function sleep(delay: number) { logger.setVerbose(process.argv.includes('--verbose')); const errorStage = process.argv - .find(arg => arg.startsWith('--error=')) + .findLast(arg => arg.startsWith('--error=')) + ?.split('=')[1]; + +const cwd = process.argv + .findLast(arg => arg.startsWith('--cwd=')) ?.split('=')[1]; try { @@ -35,14 +39,18 @@ try { `Running plugin "ESLint" ${ansis.gray('[1/2]')}`, async () => { const bin = 'npx eslint . --format=json'; - await logger.command(bin, async () => { - await sleep(3000); - if (errorStage === 'plugin') { - logger.info('Configuration file not found.'); - throw new Error(`Command ${ansis.bold(bin)} exited with code 1`); - } - logger.debug('All files pass linting.'); - }); + await logger.command( + bin, + async () => { + await sleep(3000); + if (errorStage === 'plugin') { + logger.debug('Configuration file not found.', { force: true }); + throw new Error(`Command ${ansis.bold(bin)} exited with code 1`); + } + logger.debug('All files pass linting.'); + }, + { cwd }, + ); logger.info('Found 0 lint problems'); diff --git a/packages/utils/project.json b/packages/utils/project.json index 135dc661b..0ddab978c 100644 --- a/packages/utils/project.json +++ b/packages/utils/project.json @@ -43,6 +43,9 @@ }, "error-upload": { "args": ["--error=upload"] + }, + "custom-cwd": { + "args": ["--cwd=src"] } } } diff --git a/packages/utils/src/lib/execute-process.unit.test.ts b/packages/utils/src/lib/execute-process.int.test.ts similarity index 76% rename from packages/utils/src/lib/execute-process.unit.test.ts rename to packages/utils/src/lib/execute-process.int.test.ts index 59728ccaf..8c7e5d97c 100644 --- a/packages/utils/src/lib/execute-process.unit.test.ts +++ b/packages/utils/src/lib/execute-process.int.test.ts @@ -2,6 +2,7 @@ import { ChildProcess } from 'node:child_process'; import { describe, expect, it, vi } from 'vitest'; import { getAsyncProcessRunnerConfig } from '@code-pushup/test-utils'; import { type ProcessObserver, executeProcess } from './execute-process.js'; +import { logger } from './logger.js'; describe('executeProcess', () => { const spyObserver: ProcessObserver = { @@ -12,13 +13,9 @@ describe('executeProcess', () => { }; const errorSpy = vi.fn(); - beforeEach(() => { - vi.clearAllMocks(); - }); - it('should work with node command `node -v`', async () => { const processResult = await executeProcess({ - command: `node`, + command: 'node', args: ['-v'], observer: spyObserver, }); @@ -32,7 +29,7 @@ describe('executeProcess', () => { it('should work with npx command `npx --help`', async () => { const processResult = await executeProcess({ - command: `npx`, + command: 'npx', args: ['--help'], observer: spyObserver, }); @@ -101,4 +98,42 @@ describe('executeProcess', () => { expect(spyObserver.onError).not.toHaveBeenCalled(); expect(spyObserver.onComplete).toHaveBeenCalledOnce(); }); + + it('should show spinner with serialized command and args', async () => { + await executeProcess({ command: 'echo', args: ['hello'] }); + expect(logger.command).toHaveBeenCalledWith( + 'echo hello', + expect.any(Function), + ); + }); + + it('should log stdout and stderr if verbose', async () => { + await executeProcess( + getAsyncProcessRunnerConfig({ interval: 10, runs: 2, throwError: false }), + ); + expect(logger.debug).toHaveBeenCalledWith( + ` +process:start with interval: 10, runs: 2, throwError: false +process:update +process:update +process:complete +`.trimStart(), + ); + }); + + it('should log stdout and stderr if process failed', async () => { + await expect( + executeProcess( + getAsyncProcessRunnerConfig({ + interval: 10, + runs: 1, + throwError: true, + }), + ), + ).rejects.toThrow(); + expect(logger.debug).toHaveBeenCalledWith( + expect.stringMatching(/process:start.*Error: dummy-error/s), + { force: true }, + ); + }); }); diff --git a/packages/utils/src/lib/execute-process.ts b/packages/utils/src/lib/execute-process.ts index d1fa98a3f..e05bbf444 100644 --- a/packages/utils/src/lib/execute-process.ts +++ b/packages/utils/src/lib/execute-process.ts @@ -6,63 +6,71 @@ import { spawn, } from 'node:child_process'; import type { Readable, Writable } from 'node:stream'; -import { isVerbose } from './env.js'; -import { formatCommandLog } from './format-command-log.js'; -import { ui } from './logging.js'; -import { calcDuration } from './reports/utils.js'; +import { logger } from './logger.js'; /** * Represents the process result. - * @category Types - * @public - * @property {string} stdout - The stdout of the process. - * @property {string} stderr - The stderr of the process. - * @property {number | null} code - The exit code of the process. */ export type ProcessResult = { + /** The full command with args that was executed. */ + bin: string; + /** The exit code of the process (`null` if terminated by signal). */ + code: number | null; + /** The signal which terminated the process, if any. */ + signal: NodeJS.Signals | null; + /** The standard output from the process. */ stdout: string; + /** The standard error from the process. */ stderr: string; - code: number | null; - date: string; - duration: number; }; /** * Error class for process errors. * Contains additional information about the process result. - * @category Error - * @public - * @class - * @extends Error + * * @example - * const result = await executeProcess({}) - * .catch((error) => { + * const result = await executeProcess({ ... }).catch((error) => { * if (error instanceof ProcessError) { - * console.error(error.code); - * console.error(error.stderr); - * console.error(error.stdout); + * console.error(error.message); + * console.error(error.code); + * console.error(error.stderr); + * console.error(error.stdout); * } * }); * */ export class ProcessError extends Error { + bin: string; code: number | null; - stderr: string; - stdout: string; + signal: NodeJS.Signals | null; + // attributes hidden behind getters so they're not printed in uncaught errors (too verbose) + #stdout: string; + #stderr: string; constructor(result: ProcessResult) { - super(result.stderr); + const message = result.signal + ? `Process terminated by ${result.signal}` + : `Process failed with exit code ${result.code}`; + super(message); + this.bin = result.bin; this.code = result.code; - this.stderr = result.stderr; - this.stdout = result.stdout; + this.signal = result.signal; + this.#stdout = result.stdout; + this.#stderr = result.stderr; + } + + get stdout() { + return this.#stdout; + } + + get stderr() { + return this.#stderr; } } /** * Process config object. Contains the command, args and observer. - * @param cfg - process config object with command, args and observer (optional) - * @category Types - * @public + * @param cfg Process config object with command, args and observer (optional) * @property {string} command - The command to execute. * @property {string[]} args - The arguments for the command. * @property {ProcessObserver} observer - The observer for the process. @@ -77,15 +85,15 @@ export class ProcessError extends Error { * * // node command * const cfg = { - * command: 'node', - * args: ['--version'] + * command: 'node', + * args: ['--version'] * }; * * // npx command * const cfg = { - * command: 'npx', - * args: ['--version'] - * + * command: 'npx', + * args: ['--version'] + * }; */ export type ProcessConfig = Omit< SpawnOptionsWithStdioTuple, @@ -98,22 +106,21 @@ export type ProcessConfig = Omit< }; /** - * Process observer object. Contains the onStdout, error and complete function. - * @category Types - * @public - * @property {function} onStdout - The onStdout function of the observer (optional). - * @property {function} onError - The error function of the observer (optional). - * @property {function} onComplete - The complete function of the observer (optional). + * Process observer object. * * @example * const observer = { - * onStdout: (stdout) => console.info(stdout) - * } + * onStdout: (stdout) => console.info(stdout) + * } */ export type ProcessObserver = { + /** Called when the `stdout` stream receives new data (optional). */ onStdout?: (stdout: string, sourceProcess?: ChildProcess) => void; + /** Called when the `stdout` stream receives new data (optional). */ onStderr?: (stderr: string, sourceProcess?: ChildProcess) => void; + /** Called when the process ends in an error (optional). */ onError?: (error: ProcessError) => void; + /** Called when the process ends successfully (optional). */ onComplete?: () => void; }; @@ -148,52 +155,60 @@ export type ProcessObserver = { export function executeProcess(cfg: ProcessConfig): Promise { const { command, args, observer, ignoreExitCode = false, ...options } = cfg; const { onStdout, onStderr, onError, onComplete } = observer ?? {}; - const date = new Date().toISOString(); - const start = performance.now(); - if (isVerbose()) { - ui().logger.log( - formatCommandLog(command, args, `${cfg.cwd ?? process.cwd()}`), - ); - } + const bin = [command, ...(args ?? [])].join(' '); - return new Promise((resolve, reject) => { - // shell:true tells Windows to use shell command for spawning a child process - const spawnedProcess = spawn(command, args ?? [], { - shell: true, - windowsHide: true, - ...options, - }) as ChildProcessByStdio; + return logger.command( + bin, + () => + new Promise((resolve, reject) => { + const spawnedProcess = spawn(command, args ?? [], { + // shell:true tells Windows to use shell command for spawning a child process + // https://stackoverflow.com/questions/60386867/node-spawn-child-process-not-working-in-windows + shell: true, + windowsHide: true, + ...options, + }) as ChildProcessByStdio; - // eslint-disable-next-line functional/no-let - let stdout = ''; - // eslint-disable-next-line functional/no-let - let stderr = ''; + // eslint-disable-next-line functional/no-let + let stdout = ''; + // eslint-disable-next-line functional/no-let + let stderr = ''; + // eslint-disable-next-line functional/no-let + let output = ''; // interleaved stdout and stderr - spawnedProcess.stdout.on('data', data => { - stdout += String(data); - onStdout?.(String(data), spawnedProcess); - }); + spawnedProcess.stdout.on('data', (data: unknown) => { + const message = String(data); + stdout += message; + output += message; + onStdout?.(message, spawnedProcess); + }); - spawnedProcess.stderr.on('data', data => { - stderr += String(data); - onStderr?.(String(data), spawnedProcess); - }); + spawnedProcess.stderr.on('data', (data: unknown) => { + const message = String(data); + stderr += message; + output += message; + onStderr?.(message, spawnedProcess); + }); - spawnedProcess.on('error', err => { - stderr += err.toString(); - }); + spawnedProcess.on('error', error => { + reject(error); + }); - spawnedProcess.on('close', code => { - const timings = { date, duration: calcDuration(start) }; - if (code === 0 || ignoreExitCode) { - onComplete?.(); - resolve({ code, stdout, stderr, ...timings }); - } else { - const errorMsg = new ProcessError({ code, stdout, stderr, ...timings }); - onError?.(errorMsg); - reject(errorMsg); - } - }); - }); + spawnedProcess.on('close', (code, signal) => { + const result: ProcessResult = { bin, code, signal, stdout, stderr }; + if (code === 0 || ignoreExitCode) { + logger.debug(output); + onComplete?.(); + resolve(result); + } else { + // ensure stdout and stderr are logged to help debug failure + logger.debug(output, { force: true }); + const error = new ProcessError(result); + onError?.(error); + reject(error); + } + }); + }), + ); } diff --git a/packages/utils/src/lib/format-command-log.int.test.ts b/packages/utils/src/lib/format-command-log.int.test.ts deleted file mode 100644 index 28a916a55..000000000 --- a/packages/utils/src/lib/format-command-log.int.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -import path from 'node:path'; -import { describe, expect, it } from 'vitest'; -import { removeColorCodes } from '@code-pushup/test-utils'; -import { formatCommandLog } from './format-command-log.js'; - -describe('formatCommandLog', () => { - it('should format simple command', () => { - const result = removeColorCodes( - formatCommandLog('npx', ['command', '--verbose']), - ); - - expect(result).toBe('$ npx command --verbose'); - }); - - it('should format simple command with explicit process.cwd()', () => { - const result = removeColorCodes( - formatCommandLog('npx', ['command', '--verbose'], process.cwd()), - ); - - expect(result).toBe('$ npx command --verbose'); - }); - - it('should format simple command with relative cwd', () => { - const result = removeColorCodes( - formatCommandLog('npx', ['command', '--verbose'], './wololo'), - ); - - expect(result).toBe(`wololo $ npx command --verbose`); - }); - - it('should format simple command with absolute non-current path converted to relative', () => { - const result = removeColorCodes( - formatCommandLog( - 'npx', - ['command', '--verbose'], - path.join(process.cwd(), 'tmp'), - ), - ); - expect(result).toBe('tmp $ npx command --verbose'); - }); - - it('should format simple command with relative cwd in parent folder', () => { - const result = removeColorCodes( - formatCommandLog('npx', ['command', '--verbose'], '..'), - ); - - expect(result).toBe(`.. $ npx command --verbose`); - }); - - it('should format simple command using relative path to parent directory', () => { - const result = removeColorCodes( - formatCommandLog( - 'npx', - ['command', '--verbose'], - path.dirname(process.cwd()), - ), - ); - - expect(result).toBe('.. $ npx command --verbose'); - }); -}); diff --git a/packages/utils/src/lib/format-command-log.ts b/packages/utils/src/lib/format-command-log.ts deleted file mode 100644 index 0ce5a89cd..000000000 --- a/packages/utils/src/lib/format-command-log.ts +++ /dev/null @@ -1,27 +0,0 @@ -import ansis from 'ansis'; -import path from 'node:path'; - -/** - * Formats a command string with optional cwd prefix and ANSI colors. - * - * @param {string} command - The command to execute. - * @param {string[]} args - Array of command arguments. - * @param {string} [cwd] - Optional current working directory for the command. - * @returns {string} - ANSI-colored formatted command string. - */ -export function formatCommandLog( - command: string, - args: string[] = [], - cwd: string = process.cwd(), -): string { - const relativeDir = path.relative(process.cwd(), cwd); - - return [ - ...(relativeDir && relativeDir !== '.' - ? [ansis.italic(ansis.gray(relativeDir))] - : []), - ansis.yellow('$'), - ansis.gray(command), - ansis.gray(args.map(arg => arg).join(' ')), - ].join(' '); -} diff --git a/packages/utils/src/lib/logger.int.test.ts b/packages/utils/src/lib/logger.int.test.ts index 8a015237d..2b5e71abc 100644 --- a/packages/utils/src/lib/logger.int.test.ts +++ b/packages/utils/src/lib/logger.int.test.ts @@ -1,6 +1,7 @@ import ansis from 'ansis'; import cliSpinners from 'cli-spinners'; import os from 'node:os'; +import path from 'node:path'; import process from 'node:process'; import type { MockInstance } from 'vitest'; import { Logger } from './logger.js'; @@ -115,6 +116,18 @@ ${ansis.red('Failed to load config')} expect(output).toBe(''); }); + it('should print debug logs if not verbose but force flag is used', () => { + vi.stubEnv('CP_VERBOSE', 'false'); + + new Logger().debug('Found config file code-pushup.config.js', { + force: true, + }); + + expect(output).toBe( + `${ansis.gray('Found config file code-pushup.config.js')}\n`, + ); + }); + it('should set verbose flag and environment variable', () => { vi.stubEnv('CP_VERBOSE', 'false'); const logger = new Logger(); @@ -471,14 +484,14 @@ ${ansis.red('✖')} Uploading report to portal → ${ansis.red('GraphQL error: I it('should use colored dollar prefix for commands (success)', async () => { const command = new Logger().command( 'npx eslint . --format=json', - async () => {}, + async () => ({ code: 0 }), ); expect(output).toBe( `${ansis.cyan('⠋')} ${ansis.blue('$')} npx eslint . --format=json`, ); - await expect(command).resolves.toBeUndefined(); + await expect(command).resolves.toEqual({ code: 0 }); expect(output).toBe( `${ansis.green('✔')} ${ansis.green('$')} npx eslint . --format=json ${ansis.gray('(42 ms)')}\n`, @@ -503,6 +516,42 @@ ${ansis.red('✖')} Uploading report to portal → ${ansis.red('GraphQL error: I `${ansis.red('✖')} ${ansis.red('$')} npx eslint . --format=json\n`, ); }); + + it("should print command's working directory if it differs from `process.cwd()`", async () => { + const command = new Logger().command( + 'npx eslint . --format=json', + async () => {}, + { cwd: 'src' }, + ); + + expect(output).toBe( + `${ansis.cyan('⠋')} ${ansis.blue('src')} ${ansis.blue('$')} npx eslint . --format=json`, + ); + + await expect(command).resolves.toBeUndefined(); + + expect(output).toBe( + `${ansis.green('✔')} ${ansis.blue('src')} ${ansis.green('$')} npx eslint . --format=json ${ansis.gray('(42 ms)')}\n`, + ); + }); + + it('should print relative working directory if absoluted path provided', async () => { + const command = new Logger().command( + 'npx eslint . --format=json', + async () => {}, + { cwd: path.join(process.cwd(), 'src') }, + ); + + expect(output).toBe( + `${ansis.cyan('⠋')} ${ansis.blue('src')} ${ansis.blue('$')} npx eslint . --format=json`, + ); + + await expect(command).resolves.toBeUndefined(); + + expect(output).toBe( + `${ansis.green('✔')} ${ansis.blue('src')} ${ansis.green('$')} npx eslint . --format=json ${ansis.gray('(42 ms)')}\n`, + ); + }); }); describe('spinners + groups', () => { diff --git a/packages/utils/src/lib/logger.ts b/packages/utils/src/lib/logger.ts index 6ab059a44..c8ba41033 100644 --- a/packages/utils/src/lib/logger.ts +++ b/packages/utils/src/lib/logger.ts @@ -1,5 +1,6 @@ import ansis, { type AnsiColors } from 'ansis'; import os from 'node:os'; +import path from 'node:path'; import ora, { type Ora } from 'ora'; import { dateToUnixTimestamp } from './dates.js'; import { isEnvVarEnabled } from './env.js'; @@ -112,9 +113,11 @@ export class Logger { * logger.debug('Running ESLint version 9.16.0'); * * @param message Debug text + * @param options Additional options + * @param options.force Print debug message even if verbose flag is not set */ - debug(message: string): void { - if (this.#isVerbose) { + debug(message: string, options?: { force?: boolean }): void { + if (this.#isVerbose || options?.force) { this.#log(message, 'gray'); } } @@ -185,8 +188,8 @@ export class Logger { * @param title Display text used as pending message. * @param worker Asynchronous implementation. Returned promise determines spinner status and final message. Support for inner logs has some limitations (described above). */ - task(title: string, worker: () => Promise): Promise { - return this.#spinner(worker, { + async task(title: string, worker: () => Promise): Promise { + await this.#spinner(worker, { pending: title, success: value => value, failure: error => `${title} → ${ansis.red(`${error}`)}`, @@ -198,6 +201,8 @@ export class Logger { * * A `$`-prefix is added. Its color indicates the status (blue=pending, green=success, red=failure). * + * If the command's working directory isn't `process.cwd()`, a relative path is prefixed to the output. + * * @example * await logger.command('npx eslint . --format=json', async () => { * // ... @@ -205,12 +210,20 @@ export class Logger { * * @param bin Command string with arguments. * @param worker Asynchronous execution of the command (not implemented by the logger). + * @param options Custom CWD path where the command is executed (default is `process.cwd()`). + * @template T Type of resolved worker value. */ - command(bin: string, worker: () => Promise): Promise { + command( + bin: string, + worker: () => Promise, + options?: { cwd?: string }, + ): Promise { + const cwd = options?.cwd && path.relative(process.cwd(), options.cwd); + const cwdPrefix = cwd ? `${ansis.blue(cwd)} ` : ''; return this.#spinner(worker, { - pending: `${ansis.blue('$')} ${bin}`, - success: () => `${ansis.green('$')} ${bin}`, - failure: () => `${ansis.red('$')} ${bin}`, + pending: `${cwdPrefix}${ansis.blue('$')} ${bin}`, + success: () => `${cwdPrefix}${ansis.green('$')} ${bin}`, + failure: () => `${cwdPrefix}${ansis.red('$')} ${bin}`, }); } @@ -356,7 +369,7 @@ export class Logger { success: (value: T) => string; failure: (error: unknown) => string; }, - ): Promise { + ): Promise { if (this.#activeSpinner) { throw new Error( 'Internal Logger error - concurrent spinners are not supported', @@ -422,6 +435,8 @@ export class Logger { if (result.status === 'rejected') { throw result.reason; } + + return result.value; } #log(message: string, color?: AnsiColors): void { diff --git a/testing/test-setup/src/lib/logger.mock.ts b/testing/test-setup/src/lib/logger.mock.ts index 6efe9f92f..c0c6cdb92 100644 --- a/testing/test-setup/src/lib/logger.mock.ts +++ b/testing/test-setup/src/lib/logger.mock.ts @@ -34,9 +34,7 @@ beforeAll(async () => { vi.spyOn(logger, 'task').mockImplementation(async (_, worker) => { await worker(); }), - vi.spyOn(logger, 'command').mockImplementation(async (_, worker) => { - await worker(); - }), + vi.spyOn(logger, 'command').mockImplementation((_, worker) => worker()), ); } }); diff --git a/testing/test-utils/src/lib/utils/execute-process-helper.mock.ts b/testing/test-utils/src/lib/utils/execute-process-helper.mock.ts index 0f9290acb..81a71e61a 100644 --- a/testing/test-utils/src/lib/utils/execute-process-helper.mock.ts +++ b/testing/test-utils/src/lib/utils/execute-process-helper.mock.ts @@ -1,6 +1,10 @@ import path from 'node:path'; +import { fileURLToPath } from 'node:url'; -const asyncProcessPath = path.join(__dirname, './execute-process.mock.mjs'); +const asyncProcessPath = path.join( + fileURLToPath(path.dirname(import.meta.url)), + './execute-process.mock.mjs', +); /** * Helps to get an async process runner config for testing.