Skip to content

Commit 8f8934b

Browse files
authored
Added retry logic to assertLogContainsBadExitInformation in case logger is still writing/flushing (aws#739)
1 parent a88ca91 commit 8f8934b

File tree

2 files changed

+81
-42
lines changed

2 files changed

+81
-42
lines changed

src/shared/loggerUtils.ts

+2-4
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,8 @@ export class TestLogger {
3131
}
3232
}
3333

34-
public async logContainsText(str: string): Promise<boolean> {
35-
const logText = await filesystemUtilities.readFileAsString(TestLogger.getLogPath(this.logFolder))
36-
37-
return logText.includes(str)
34+
public get logPath(): string {
35+
return TestLogger.getLogPath(this.logFolder)
3836
}
3937

4038
public static async createTestLogger(): Promise<TestLogger> {

src/test/shared/sam/cli/testSamCliProcessInvoker.ts

+79-38
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import * as assert from 'assert'
77
import { SpawnOptions } from 'child_process'
88

9+
import { access } from '../../../../shared/filesystem'
10+
import { readFileAsString } from '../../../../shared/filesystemUtilities'
911
import { TestLogger } from '../../../../shared/loggerUtils'
1012
import {
1113
makeRequiredSamCliProcessInvokeOptions,
@@ -15,10 +17,7 @@ import {
1517
import { ChildProcessResult } from '../../../../shared/utilities/childProcess'
1618

1719
export class TestSamCliProcessInvoker implements SamCliProcessInvoker {
18-
public constructor(
19-
private readonly onInvoke: (spawnOptions: SpawnOptions, ...args: any[]) => ChildProcessResult
20-
) {
21-
}
20+
public constructor(private readonly onInvoke: (spawnOptions: SpawnOptions, ...args: any[]) => ChildProcessResult) {}
2221

2322
public async invoke(options?: SamCliProcessInvokeOptions): Promise<ChildProcessResult> {
2423
const invokeOptions = makeRequiredSamCliProcessInvokeOptions(options)
@@ -28,7 +27,6 @@ export class TestSamCliProcessInvoker implements SamCliProcessInvoker {
2827
}
2928

3029
export class BadExitCodeSamCliProcessInvoker extends TestSamCliProcessInvoker {
31-
3230
public exitCode: number
3331
public error: Error
3432
public stdout: string
@@ -38,7 +36,7 @@ export class BadExitCodeSamCliProcessInvoker extends TestSamCliProcessInvoker {
3836
exitCode = -1,
3937
error = new Error('Bad Result'),
4038
stdout = 'stdout message',
41-
stderr = 'stderr message',
39+
stderr = 'stderr message'
4240
}: {
4341
exitCode?: number
4442
error?: Error
@@ -60,7 +58,7 @@ export class BadExitCodeSamCliProcessInvoker extends TestSamCliProcessInvoker {
6058
exitCode: this.exitCode,
6159
error: this.error,
6260
stdout: this.stdout,
63-
stderr: this.stderr,
61+
stderr: this.stderr
6462
}
6563

6664
return result
@@ -73,51 +71,94 @@ export class FakeChildProcessResult implements ChildProcessResult {
7371
public stdout: string
7472
public stderr: string
7573

76-
public constructor(
77-
{
78-
exitCode = 0,
79-
stdout = '',
80-
stderr = '',
81-
...params
82-
}: Partial<ChildProcessResult>
83-
) {
74+
public constructor({ exitCode = 0, stdout = '', stderr = '', ...params }: Partial<ChildProcessResult>) {
8475
this.exitCode = exitCode
8576
this.error = params.error
8677
this.stdout = stdout
8778
this.stderr = stderr
8879
}
8980
}
9081

91-
export function assertErrorContainsBadExitMessage(
92-
actualError: Error,
93-
sourceErrorMessage: string
94-
) {
82+
export function assertErrorContainsBadExitMessage(actualError: Error, sourceErrorMessage: string) {
9583
assert.strictEqual(
96-
actualError.message, `Error with child process: ${sourceErrorMessage}`,
84+
actualError.message,
85+
`Error with child process: ${sourceErrorMessage}`,
9786
'Unexpected error message'
9887
)
9988
}
10089

10190
export async function assertLogContainsBadExitInformation(
10291
logger: TestLogger,
10392
errantChildProcessResult: ChildProcessResult,
104-
expectedExitCode: number,
93+
expectedExitCode: number
10594
): Promise<void> {
106-
assert.ok(
107-
// tslint:disable-next-line:max-line-length
108-
await logger.logContainsText(`Unexpected exitcode (${errantChildProcessResult.exitCode}), expecting (${expectedExitCode})`),
109-
'Log message missing for exit code'
110-
)
111-
assert.ok(
112-
await logger.logContainsText(`Error: ${errantChildProcessResult.error}`),
113-
'Log message missing for error'
114-
)
115-
assert.ok(
116-
await logger.logContainsText(`stderr: ${errantChildProcessResult.stderr}`),
117-
'Log message missing for stderr'
118-
)
119-
assert.ok(
120-
await logger.logContainsText(`stdout: ${errantChildProcessResult.stdout}`),
121-
'Log message missing for stdout'
122-
)
95+
const logPath = logger.logPath
96+
const expectedTexts = [
97+
{
98+
text: `Unexpected exitcode (${errantChildProcessResult.exitCode}), expecting (${expectedExitCode})`,
99+
verifyMessage: 'Log message missing for exit code'
100+
},
101+
{ text: `Error: ${errantChildProcessResult.error}`, verifyMessage: 'Log message missing for error' },
102+
{ text: `stderr: ${errantChildProcessResult.stderr}`, verifyMessage: 'Log message missing for stderr' },
103+
{ text: `stdout: ${errantChildProcessResult.stdout}`, verifyMessage: 'Log message missing for stdout' }
104+
]
105+
106+
// Give the log a chance to get created/flushed
107+
await retryOnError({
108+
description: 'Wait for log file to exist',
109+
fn: async () => await access(logPath),
110+
retries: 20,
111+
delayMilliseconds: 50,
112+
failureMessage: `Could not find log file: ${logPath}`
113+
})
114+
115+
await retryOnError({
116+
description: 'Wait for expected text to appear in log',
117+
fn: async () => {
118+
const logText = await readFileAsString(logPath)
119+
const verifyMessage = verifyLogText(logText, expectedTexts)
120+
if (verifyMessage) {
121+
console.log(verifyMessage)
122+
throw new Error(verifyMessage)
123+
}
124+
},
125+
retries: 20,
126+
delayMilliseconds: 50,
127+
failureMessage: 'Did not find expected log contents'
128+
})
129+
}
130+
131+
function verifyLogText(text: string, expectedTexts: { text: string; verifyMessage: string }[]): string | undefined {
132+
for (const entry of expectedTexts) {
133+
if (!text.includes(entry.text)) {
134+
return entry.verifyMessage
135+
}
136+
}
137+
138+
return undefined
139+
}
140+
141+
async function retryOnError(parameters: {
142+
description: string
143+
retries: number
144+
delayMilliseconds: number
145+
failureMessage: string
146+
fn(): Promise<void>
147+
}): Promise<void> {
148+
for (let attempt = 0; attempt < parameters.retries; attempt++) {
149+
try {
150+
console.log(`${parameters.description}: Attempt ${attempt + 1}`)
151+
await parameters.fn()
152+
153+
return
154+
} catch (err) {
155+
if (attempt + 1 >= parameters.retries) {
156+
assert.fail(parameters.failureMessage)
157+
}
158+
}
159+
160+
await new Promise<any>(resolve => setTimeout(resolve, parameters.delayMilliseconds))
161+
}
162+
163+
assert.fail(parameters.failureMessage)
123164
}

0 commit comments

Comments
 (0)