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

View File

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

View File

@ -1,8 +1,10 @@
import * as exec from '@actions/exec' import * as exec from '@actions/exec'
import {StringDecoder} from 'string_decoder'
import { import {
CommandRunnerContext, CommandRunnerContext,
CommandRunnerMiddleware, CommandRunnerMiddleware,
CommandRunnerMiddlewarePromisified CommandRunnerMiddlewarePromisified,
CommandRunnerOptions
} from './types' } from './types'
export const promisifyCommandRunnerMiddleware = export const promisifyCommandRunnerMiddleware =
@ -36,41 +38,14 @@ export const composeCommandRunnerMiddleware =
export class CommandRunnerBase<S = unknown> { export class CommandRunnerBase<S = unknown> {
private middleware: CommandRunnerMiddlewarePromisified[] = [] private middleware: CommandRunnerMiddlewarePromisified[] = []
private tmpArgs: string[] = []
constructor( constructor(
private commandLine: string, private commandLine: string,
private args: string[] = [], private args: string[] = [],
private options: exec.ExecOptions = {}, private options: CommandRunnerOptions,
private executor: typeof exec.getExecOutput = exec.getExecOutput 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 { use(middleware: CommandRunnerMiddleware<S>): this {
this.middleware.push( this.middleware.push(
promisifyCommandRunnerMiddleware( promisifyCommandRunnerMiddleware(
@ -88,14 +63,17 @@ export class CommandRunnerBase<S = unknown> {
args?: string[], args?: string[],
/* overrides options for this specific execution if not undefined */ /* overrides options for this specific execution if not undefined */
options?: exec.ExecOptions options?: CommandRunnerOptions
): Promise<CommandRunnerContext<S>> { ): Promise<CommandRunnerContext<S>> {
const tmpArgs = this.getTmpArgs() const requiredOptions: exec.ExecOptions = {
ignoreReturnCode: true,
failOnStdErr: false
}
const context: CommandRunnerContext<S> = { const context: CommandRunnerContext<S> = {
commandLine: commandLine ?? this.commandLine, commandLine: commandLine ?? this.commandLine,
args: args ?? tmpArgs ?? this.args, args: args ?? this.args,
options: options ?? this.options, options: {...(options ?? this.options), ...requiredOptions},
stdout: null, stdout: null,
stderr: null, stderr: null,
execerr: null, execerr: null,
@ -104,15 +82,30 @@ export class CommandRunnerBase<S = unknown> {
} }
try { 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.commandLine,
context.args, context.args,
context.options {
...context.options,
listeners: {
...options?.listeners,
stdout: stdOutListener,
stderr: stdErrListener
}
}
) )
context.stdout = stdout
context.stderr = stderr
context.exitCode = exitCode
} catch (error) { } catch (error) {
context.execerr = error as 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') eventTypes.add('execerr')
} }
if (ctx.stderr || ctx.exitCode !== 0) { if (ctx.stderr) {
eventTypes.add('stderr') eventTypes.add('stderr')
} }
if (ctx.stdout !== null && !ctx.stdout.trim()) { if (ctx.exitCode) {
eventTypes.add('no-stdout') eventTypes.add('exitcode')
}
if (ctx.stdout) {
eventTypes.add('stdout')
} }
if (!ctx.stderr && !ctx.execerr && ctx.stdout !== null && !ctx.exitCode) { if (!ctx.stderr && !ctx.execerr && ctx.stdout !== null && !ctx.exitCode) {
@ -72,7 +76,7 @@ export const failAction: CommandRunnerAction = message => async ctx => {
return return
} }
if (events.includes('no-stdout')) { if (!events.includes('stdout')) {
core.setFailed( core.setFailed(
`The command "${ctx.commandLine}" finished with exit code ${ctx.exitCode} and produced an empty output.` `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( throw new Error(
`The command "${ctx.commandLine}" finished with exit code ${ctx.exitCode} and produced an empty output.` `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 return
} }
if (events.includes('no-stdout')) { if (!events.includes('stdout')) {
core.warning(messageText) core.warning(messageText)
next() next()
return return
@ -176,7 +180,7 @@ export const produceLog: CommandRunnerAction = message => async (ctx, next) => {
return return
} }
if (events.includes('no-stdout')) { if (!events.includes('stdout')) {
core.warning( core.warning(
`The command "${ctx.commandLine}" finished with exit code ${ctx.exitCode} and produced an empty output.` `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' export type CommandRunnerActionType = 'throw' | 'fail' | 'log'
/* Command runner event types as used internally passed to middleware for the user */ /* 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 */ /* Command runner event types as used by the user for filtering results */
export type CommandRunnerEventTypeExtended = export type CommandRunnerEventTypeExtended =
| CommandRunnerEventType | CommandRunnerEventType
| `!${CommandRunnerEventType}` | `!${CommandRunnerEventType}`
export type CommandRunnerOptions = Omit<
exec.ExecOptions,
'failOnStdErr' | 'ignoreReturnCode'
>