Skip to content

Commit 85dbc6b

Browse files
authored
feat: configurable step timeouts (#12)
1 parent bff4533 commit 85dbc6b

File tree

6 files changed

+54
-3
lines changed

6 files changed

+54
-3
lines changed

lib/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export type { Context, EcosystemSuite } from './test-suite'
22
export { TestSuite, TestCase, Step } from './test-suite'
33
export * as steps from './steps'
4+
export type { Maybe } from './types'
45

56
// test suite templates
67
export { installAndTest } from './install-and-test'

lib/test-suite.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import deepmerge from 'deepmerge'
22
import type { Maybe } from './types'
3+
import { strict as assert } from 'node:assert'
34

45
/**
56
* Test suites have similar checks over a bunch of cases. Each case normally
@@ -160,6 +161,13 @@ export interface Step {
160161
* Current working directory to use when running commands for this step.
161162
*/
162163
cwd?: string
164+
/**
165+
* Timeout for this step in milliseconds. If the step takes longer than
166+
* this, it fails. By default, no timeout is set.
167+
*
168+
* @min 1
169+
*/
170+
timeout?: number
163171
/**
164172
* Buildkite-specific configuration. These aren't used for other kinds of runners.
165173
*/
@@ -225,4 +233,14 @@ export namespace Step {
225233

226234
return true
227235
}
236+
237+
export function validate(step: Step): void {
238+
assert(step.run?.length, 'Step must have at least one command')
239+
if (step.timeout != null) {
240+
assert(
241+
step.timeout > 0,
242+
`Timeout must be greater than 0, got ${step.timeout}`
243+
)
244+
}
245+
}
228246
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// Bun Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`renderStep respects \`step.timeout\` 1`] = `
4+
"# Step: echo "hello world"
5+
timeout --verbose --kill-after=2000 1000 (
6+
echo "hello world"
7+
)"
8+
`;

src/local.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import assert from 'assert'
22
import path from 'path'
33
import { Command } from 'commander'
44
import {
5+
Step,
56
TestSuite,
67
type Context,
78
type EcosystemSuite,
@@ -214,7 +215,8 @@ async function runAllTests({
214215

215216
async function runCase(testCase: TestCase): Promise<number> {
216217
for (const step of testCase.steps) {
217-
const { run, env: stepEnv, cwd, name } = step
218+
Step.validate(step)
219+
const { run, env: stepEnv, cwd, name, timeout } = step
218220
const env = Object.assign(
219221
pick(process.env, inheritEnvVarNames),
220222
testCase.env,
@@ -226,6 +228,7 @@ async function runCase(testCase: TestCase): Promise<number> {
226228
stdio: ['pipe', 'inherit', 'inherit'],
227229
cwd,
228230
env,
231+
timeout,
229232
})
230233
for (const cmd of run) {
231234
child.stdin.write(cmd + '\n')

src/shell.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,5 +111,17 @@ describe(renderStep, () => {
111111
})
112112
expect(await result.exited).toBe(0)
113113
})
114+
}) // </ when a basic step is rendered>
115+
116+
it('respects `step.timeout`', () => {
117+
const actual = renderStep(
118+
Step.from('echo "hello world"', {
119+
timeout: 1000,
120+
})
121+
)
122+
expect(actual).not.toBeEmpty()
123+
const rendered = actual.join('\n')
124+
expect(rendered).toMatchSnapshot()
125+
expect(rendered).toMatch(/timeout.*1000/)
114126
})
115127
})

src/shell.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import assert from 'node:assert'
2-
import type { Step, TestCase, TestSuite } from '../lib'
2+
import type { Step, TestCase, TestSuite, Maybe } from '../lib'
33

44
/**
55
* Render a test suite into a shell script.
@@ -76,7 +76,7 @@ export function renderStep(step: Step): string[] {
7676

7777
const lines = [`# Step: ${name ?? [run[0]]}`]
7878
if (name) lines.push(`echo '${name.replaceAll("'", "\\'")}'`)
79-
lines.push(...subshell(indent(cmds)))
79+
lines.push(...withTimeout(step.timeout)(subshell(indent(cmds))))
8080

8181
return lines
8282
}
@@ -101,3 +101,12 @@ const withDir = (dir: string | undefined) =>
101101
dir ? wrap(`pushd ${dir}`, 'popd') : reifyLines //wrap('', '')
102102
/** Runs commands in a subshell */
103103
const subshell = wrap('(', ')')
104+
const withTimeout =
105+
(timeout: Maybe<number>) =>
106+
(lines: string[]): string[] => {
107+
if (timeout == null || !lines.length) return lines
108+
assert(timeout > 0)
109+
assert(lines.length > 0)
110+
lines[0] = `timeout --verbose --kill-after=${timeout * 2} ${timeout} ${lines[0]}`
111+
return lines
112+
}

0 commit comments

Comments
 (0)