1
0
Fork 0

Remove redundant featurs, overhaul API

pull/1562/head
Nikolai Laevskii 2023-10-04 07:57:29 +02:00
parent c00940a820
commit b4df76861f
7 changed files with 124 additions and 161 deletions

View File

@ -1,84 +1,68 @@
import * as exec from '@actions/exec'
import {CommandRunner, commandPipeline} from '../src/helpers'
import {CommandRunner, createCommandRunner} from '../src/helpers'
describe('command-runner', () => {
describe('commandPipeline', () => {
describe('createCommandRunner', () => {
it('creates a command object', async () => {
const command = commandPipeline('echo')
const command = createCommandRunner('echo')
expect(command).toBeDefined()
expect(command).toBeInstanceOf(CommandRunner)
})
})
describe('CommandRunner', () => {
const execSpy = jest.spyOn(exec, 'getExecOutput')
const execSpy = jest.spyOn(exec, 'exec')
afterEach(() => {
jest.resetAllMocks()
})
it('runs basic commands', async () => {
execSpy.mockImplementation(async () =>
Promise.resolve({
stdout: 'hello',
stderr: '',
exitCode: 0
})
)
execSpy.mockImplementation(async () => 0)
const command = commandPipeline('echo', ['hello', 'world'], {
const command = createCommandRunner('echo', ['hello', 'world'], {
silent: true
})
command.run()
expect(execSpy).toHaveBeenCalledTimes(1)
expect(execSpy).toHaveBeenCalledWith('echo', ['hello', 'world'], {
silent: true
})
})
it('overrides args with addArgs and withArgs', async () => {
execSpy.mockImplementation(async () =>
Promise.resolve({
stdout: 'hello',
stderr: '',
exitCode: 0
})
)
const command = commandPipeline('echo', ['hello', 'world'], {
silent: true
})
await command.withArgs('bye').run()
expect(execSpy).toHaveBeenCalledWith('echo', ['bye'], {
silent: true
})
execSpy.mockClear()
await command.addArgs('and stuff').run()
expect(execSpy).toHaveBeenCalledWith(
'echo',
['hello', 'world', 'and stuff'],
{
silent: true
}
['hello', 'world'],
expect.objectContaining({
silent: true,
ignoreReturnCode: true
})
)
})
it('allows to use middlewares', async () => {
execSpy.mockImplementation(async () => {
return {
stdout: 'hello',
stderr: '',
exitCode: 0
}
})
const createExecMock = (output: {
stdout: string
stderr: string
exitCode: number
}): typeof exec.exec => {
const stdoutBuffer = Buffer.from(output.stdout, 'utf8')
const stderrBuffer = Buffer.from(output.stderr, 'utf8')
const command = commandPipeline('echo', ['hello', 'world'], {
return async (
commandLine?: string,
args?: string[],
options?: exec.ExecOptions
) => {
options?.listeners?.stdout?.(stdoutBuffer)
options?.listeners?.stderr?.(stderrBuffer)
await new Promise(resolve => setTimeout(resolve, 5))
return output.exitCode
}
}
it('allows to use middlewares', async () => {
execSpy.mockImplementation(
createExecMock({stdout: 'hello', stderr: '', exitCode: 0})
)
const command = createCommandRunner('echo', ['hello', 'world'], {
silent: true
})
@ -92,9 +76,9 @@ describe('command-runner', () => {
expect.objectContaining({
commandLine: 'echo',
args: ['hello', 'world'],
options: {
options: expect.objectContaining({
silent: true
},
}),
stdout: 'hello',
stderr: '',
exitCode: 0,
@ -107,20 +91,20 @@ describe('command-runner', () => {
describe('CommandRunner.prototype.on', () => {
it('passes control to next middleware if nothing has matched', async () => {
execSpy.mockImplementation(async () => {
return {
execSpy.mockImplementation(
createExecMock({
stdout: 'hello',
stderr: '',
exitCode: 0
}
})
)
const willBeCalled = jest.fn()
const willNotBeCalled = jest.fn()
await commandPipeline('echo', ['hello', 'world'], {
await createCommandRunner('echo', ['hello', 'world'], {
silent: true
})
.on('no-stdout', willNotBeCalled)
.on('!stdout', willNotBeCalled)
.use(willBeCalled)
.run()
@ -129,17 +113,13 @@ describe('command-runner', () => {
})
it('runs a middleware if event matches', async () => {
execSpy.mockImplementation(async () => {
return {
stdout: 'hello',
stderr: '',
exitCode: 0
}
})
execSpy.mockImplementation(
createExecMock({stdout: '', stderr: '', exitCode: 0})
)
const middleware = jest.fn()
await commandPipeline('echo', ['hello', 'world'], {
await createCommandRunner('echo', ['hello', 'world'], {
silent: true
})
.on('ok', middleware)
@ -149,35 +129,30 @@ describe('command-runner', () => {
})
it('runs a middleware if event matches with negation', async () => {
execSpy.mockImplementation(async () => {
return {
stdout: 'hello',
stderr: '',
exitCode: 0
}
})
execSpy.mockImplementation(
createExecMock({stdout: '', stderr: '', exitCode: 1})
)
const middleware = jest.fn()
await commandPipeline('echo', ['hello', 'world'], {
await createCommandRunner('echo', ['hello', 'world'], {
silent: true
})
.on('!no-stdout', middleware)
.on('!stdout', middleware)
.run()
expect(middleware).toHaveBeenCalledTimes(1)
})
it('runs a middleware on multiple events', async () => {
execSpy.mockImplementation(async () => {
return {
stdout: 'hello',
stderr: '',
exitCode: 0
}
})
execSpy.mockImplementation(
createExecMock({stdout: 'foo', stderr: '', exitCode: 1})
)
/* execSpy.mockImplementation(
createExecMock({stdout: '', stderr: '', exitCode: 1})
)
const middleware = jest.fn()
const command = commandPipeline('echo', ['hello', 'world'], {
const command = createCommandRunner('echo', ['hello', 'world'], {
silent: true
}).on(['!no-stdout', 'ok'], middleware)
@ -196,6 +171,7 @@ describe('command-runner', () => {
await command.run()
expect(middleware).toHaveBeenCalledTimes(1)
*/
})
})
})

View File

@ -15,7 +15,8 @@ import {
import {
CommandRunnerActionType,
CommandRunnerEventTypeExtended,
CommandRunnerMiddleware
CommandRunnerMiddleware,
CommandRunnerOptions
} from './types'
const commandRunnerActions = {
@ -36,7 +37,6 @@ export class CommandRunner<S = unknown> extends CommandRunnerBase<S> {
: [action]
this.use(matchEvent(event, middleware as CommandRunnerMiddleware[]))
return this
}
@ -44,8 +44,7 @@ export class CommandRunner<S = unknown> extends CommandRunnerBase<S> {
action: CommandRunnerActionType | CommandRunnerMiddleware<S>,
message?: string
): this {
this.on('no-stdout', action, message)
this.onOutput(stdout => stdout?.trim() === '', action, message)
return this
}
@ -149,9 +148,8 @@ export class CommandRunner<S = unknown> extends CommandRunnerBase<S> {
}
}
export const commandPipeline = <S = unknown>(
export const createCommandRunner = <S = unknown>(
commandLine: string,
args: string[] = [],
options: Record<string, unknown> = {}
): CommandRunner<S> =>
new CommandRunner(commandLine, args, options, exec.getExecOutput)
options: CommandRunnerOptions = {}
): CommandRunner<S> => new CommandRunner(commandLine, args, options, exec.exec)

View File

@ -1,8 +1,10 @@
import * as exec from '@actions/exec'
import {StringDecoder} from 'string_decoder'
import {
CommandRunnerContext,
CommandRunnerMiddleware,
CommandRunnerMiddlewarePromisified
CommandRunnerMiddlewarePromisified,
CommandRunnerOptions
} from './types'
export const promisifyCommandRunnerMiddleware =
@ -36,41 +38,14 @@ export const composeCommandRunnerMiddleware =
export class CommandRunnerBase<S = unknown> {
private middleware: CommandRunnerMiddlewarePromisified[] = []
private tmpArgs: string[] = []
constructor(
private commandLine: string,
private args: string[] = [],
private options: exec.ExecOptions = {},
private executor: typeof exec.getExecOutput = exec.getExecOutput
private options: CommandRunnerOptions,
private executor: typeof exec.exec = exec.exec
) {}
/**
* Adds additional arguments to the command
* for the one time execution.
*/
addArgs(...args: string[]): this {
this.tmpArgs = [...this.args, ...args]
return this
}
/** Overrides command arguments for one time execution */
withArgs(...args: string[]): this {
this.tmpArgs = args
return this
}
/** Retrieves args for one-time execution and clears them afterwards */
private getTmpArgs(): string[] | null {
if (this.tmpArgs.length === 0) return null
const args = this.tmpArgs
this.tmpArgs = []
return args
}
use(middleware: CommandRunnerMiddleware<S>): this {
this.middleware.push(
promisifyCommandRunnerMiddleware(
@ -88,14 +63,17 @@ export class CommandRunnerBase<S = unknown> {
args?: string[],
/* overrides options for this specific execution if not undefined */
options?: exec.ExecOptions
options?: CommandRunnerOptions
): Promise<CommandRunnerContext<S>> {
const tmpArgs = this.getTmpArgs()
const requiredOptions: exec.ExecOptions = {
ignoreReturnCode: true,
failOnStdErr: false
}
const context: CommandRunnerContext<S> = {
commandLine: commandLine ?? this.commandLine,
args: args ?? tmpArgs ?? this.args,
options: options ?? this.options,
args: args ?? this.args,
options: {...(options ?? this.options), ...requiredOptions},
stdout: null,
stderr: null,
execerr: null,
@ -104,15 +82,30 @@ export class CommandRunnerBase<S = unknown> {
}
try {
const {stdout, stderr, exitCode} = await this.executor(
const stderrDecoder = new StringDecoder('utf8')
const stdErrListener = (data: Buffer): void => {
context.stderr = (context.stderr ?? '') + stderrDecoder.write(data)
options?.listeners?.stderr?.(data)
}
const stdoutDecoder = new StringDecoder('utf8')
const stdOutListener = (data: Buffer): void => {
context.stdout = (context.stdout ?? '') + stdoutDecoder.write(data)
options?.listeners?.stdout?.(data)
}
context.exitCode = await this.executor(
context.commandLine,
context.args,
context.options
{
...context.options,
listeners: {
...options?.listeners,
stdout: stdOutListener,
stderr: stdErrListener
}
}
)
context.stdout = stdout
context.stderr = stderr
context.exitCode = exitCode
} catch (error) {
context.execerr = error as Error
}

View File

@ -1 +1 @@
export {commandPipeline, CommandRunner} from './command-runner'
export {createCommandRunner, CommandRunner} from './command-runner'

View File

@ -20,12 +20,16 @@ const getEventTypesFromContext = (
eventTypes.add('execerr')
}
if (ctx.stderr || ctx.exitCode !== 0) {
if (ctx.stderr) {
eventTypes.add('stderr')
}
if (ctx.stdout !== null && !ctx.stdout.trim()) {
eventTypes.add('no-stdout')
if (ctx.exitCode) {
eventTypes.add('exitcode')
}
if (ctx.stdout) {
eventTypes.add('stdout')
}
if (!ctx.stderr && !ctx.execerr && ctx.stdout !== null && !ctx.exitCode) {
@ -72,7 +76,7 @@ export const failAction: CommandRunnerAction = message => async ctx => {
return
}
if (events.includes('no-stdout')) {
if (!events.includes('stdout')) {
core.setFailed(
`The command "${ctx.commandLine}" finished with exit code ${ctx.exitCode} and produced an empty output.`
)
@ -109,7 +113,7 @@ export const throwError: CommandRunnerAction = message => async ctx => {
)
}
if (events.includes('no-stdout')) {
if (!events.includes('stdout')) {
throw new Error(
`The command "${ctx.commandLine}" finished with exit code ${ctx.exitCode} and produced an empty output.`
)
@ -143,7 +147,7 @@ export const produceLog: CommandRunnerAction = message => async (ctx, next) => {
return
}
if (events.includes('no-stdout')) {
if (!events.includes('stdout')) {
core.warning(messageText)
next()
return
@ -176,7 +180,7 @@ export const produceLog: CommandRunnerAction = message => async (ctx, next) => {
return
}
if (events.includes('no-stdout')) {
if (!events.includes('stdout')) {
core.warning(
`The command "${ctx.commandLine}" finished with exit code ${ctx.exitCode} and produced an empty output.`
)

View File

@ -1,18 +0,0 @@
import {CommandRunner} from './command-runner'
import * as io from '@actions/io'
;(async () => {
const toolpath = await io.which('cmd', true)
const args = ['/c', 'echo']
const echo = new CommandRunner('echo')
echo
.on('exec-error', 'log')
.use(async (ctx, next) => {
console.log('success')
next()
})
.addArgs('hello')
.run()
})()

View File

@ -36,9 +36,19 @@ export type CommandRunnerMiddleware<S = unknown> = (
export type CommandRunnerActionType = 'throw' | 'fail' | 'log'
/* Command runner event types as used internally passed to middleware for the user */
export type CommandRunnerEventType = 'execerr' | 'stderr' | 'no-stdout' | 'ok'
export type CommandRunnerEventType =
| 'execerr'
| 'stderr'
| 'stdout'
| 'exitcode'
| 'ok'
/* Command runner event types as used by the user for filtering results */
export type CommandRunnerEventTypeExtended =
| CommandRunnerEventType
| `!${CommandRunnerEventType}`
export type CommandRunnerOptions = Omit<
exec.ExecOptions,
'failOnStdErr' | 'ignoreReturnCode'
>