Skip to content

Commit 5e8030e

Browse files
authored
chore(e2e-tests): force wait for initialization before writing input (#2600)
In the past, we've had a number of flaky tests that followed a pattern similar to this: 1. Start a test shell 2. Run a command using `executeLine()` 3. Make assertions about the command's output The internal logic of `executeLine()` determines that it has seen the end of the command's output by looking for the subsequent prompt (similar to how a human being in front of the shell would determine that the command has finished). However, in the above scheme, there is a race condition that can develop as follows: 1. The test shell process starts and prints part of its initial output 2. The `executeLine()` command remembers the current output position and sends the input to the process's stdin stream 3. The test shell prints its first prompt 4. The test reads the output from the shell and sees the prompt, assumes that the comamnd has finished 5. The test shell finishes evaluating the input and writes the corresponding output + a new prompt 6. The test ignores the remaining output and instead only treats the text up to the first prompt as the command output 7. An assertion made about the output fails, as the output is not coming from the command it was supposed to come from In order to avoid the likelihood of introducing such race conditions, this commit adds a guardrail to the input writing methods that ensures that the shell has reached some form of known state that has been awaited, such as seeing the first prompt or other specific output, before it allows writing to the process's input.
1 parent 7e28603 commit 5e8030e

File tree

4 files changed

+43
-18
lines changed

4 files changed

+43
-18
lines changed

packages/e2e-tests/test/e2e-direct.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,10 @@ describe('e2e direct connection', function () {
8686
const shell = this.startTestShell({
8787
args: [await rs0.connectionString()],
8888
});
89+
await shell.waitForPrompt();
8990
await shell.executeLine(`db.getSiblingDB("${dbname}").dropDatabase()`);
9091
shell.writeInputLine('exit');
92+
await shell.waitForSuccessfulExit();
9193
});
9294

9395
context('connecting to secondary members directly', function () {

packages/e2e-tests/test/e2e-fle.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ describe('FLE tests', function () {
135135
},
136136
cwd: path.join(__dirname, '..', '..', 'cli-repl', 'test', 'fixtures'),
137137
});
138+
await shell.waitForPrompt();
138139

139140
if (withEnvVarCredentials) {
140141
// Need to set up the AWS context inside the shell for enabling
@@ -926,6 +927,7 @@ describe('FLE tests', function () {
926927
await testServer.connectionString(),
927928
],
928929
});
930+
await shell.waitForPrompt();
929931
await shell.executeLine(`{
930932
const keyMongo = Mongo(
931933
db.getMongo(),

packages/e2e-tests/test/e2e.spec.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -310,10 +310,9 @@ describe('e2e', function () {
310310
// The number of newlines here matters
311311
shell.writeInput(
312312
'sleep(100);print([1,2,3,4,5,6,7,8,9,10].reduce(\n(a,b) => { return a*b; }, 1))\n\n\n\n',
313-
{ end: true }
313+
{ end: true, requireFinishedInitialization: false }
314314
);
315-
await shell.waitForSuccessfulExit();
316-
shell.assertContainsOutput('3628800');
315+
expect(await shell.waitForCleanOutput()).to.include('3628800');
317316
});
318317
it('ignores control characters in TTY input', async function () {
319318
shell = this.startTestShell({
@@ -1155,10 +1154,9 @@ describe('e2e', function () {
11551154
});
11561155
shell.writeInput(
11571156
'[db.hello()].reduce(\n() => { return 11111*11111; },0)\n\n\n',
1158-
{ end: true }
1157+
{ end: true, requireFinishedInitialization: false }
11591158
);
1160-
await shell.waitForSuccessfulExit();
1161-
shell.assertContainsOutput('123454321');
1159+
expect(await shell.waitForCleanOutput()).to.include('123454321');
11621160
});
11631161
});
11641162

@@ -2605,9 +2603,7 @@ describe('e2e', function () {
26052603

26062604
it('allows connecting to a host and running commands against it', async function () {
26072605
const connectionString = await testServer.connectionString();
2608-
await eventually(() => {
2609-
shell.assertContainsOutput('Please enter a MongoDB connection string');
2610-
});
2606+
await shell.waitForLine(/Please enter a MongoDB connection string/);
26112607
shell.writeInputLine(connectionString);
26122608
await shell.waitForPrompt();
26132609

packages/e2e-tests/test/test-shell.ts

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,16 @@ type SignalType = ChildProcess extends { kill: (signal: infer T) => any }
2020
? T
2121
: never;
2222

23+
export interface TestShellInputOptions {
24+
end?: boolean;
25+
requireFinishedInitialization?: boolean;
26+
}
27+
28+
export interface TestShellWaitForPromptOptions {
29+
timeout?: number;
30+
promptPattern?: RegExp;
31+
}
32+
2333
// Assume that prompt strings are those that end in '> ' but do not contain
2434
// < or > (so that e.g. '- <repl>' in a stack trace is not considered a prompt).
2535
const PROMPT_PATTERN = /^([^<>]*> ?)+$/m;
@@ -152,6 +162,7 @@ export class TestShell {
152162
private _output: string;
153163
private _rawOutput: string;
154164
private _onClose: Promise<number>;
165+
private _initializationKnownToBeFinished = false;
155166

156167
constructor(
157168
shellProcess: ChildProcessWithoutNullStreams,
@@ -205,11 +216,14 @@ export class TestShell {
205216
});
206217
}
207218
});
219+
// Not technically true, but in practice the patterns we wait for
220+
// are sufficient to indicate that initialization is done.
221+
this._initializationKnownToBeFinished = true;
208222
}
209223

210224
async waitForPrompt(
211225
start = 0,
212-
opts: { timeout?: number; promptPattern?: RegExp } = {}
226+
opts: TestShellWaitForPromptOptions = {}
213227
): Promise<void> {
214228
await eventually(
215229
() => {
@@ -247,10 +261,13 @@ export class TestShell {
247261
},
248262
{ ...opts }
249263
);
264+
this._initializationKnownToBeFinished = true;
250265
}
251266

252-
waitForAnyExit(): Promise<number> {
253-
return this._onClose;
267+
async waitForAnyExit(): Promise<number> {
268+
const code = await this._onClose;
269+
this._initializationKnownToBeFinished = true;
270+
return code;
254271
}
255272

256273
async waitForSuccessfulExit(): Promise<void> {
@@ -267,7 +284,7 @@ export class TestShell {
267284
}
268285

269286
async waitForPromptOrExit(
270-
opts: { timeout?: number; start?: number } = {}
287+
opts: TestShellWaitForPromptOptions & { start?: number } = {}
271288
): Promise<TestShellStartupResult> {
272289
return Promise.race([
273290
this.waitForPrompt(opts.start ?? 0, opts).then(
@@ -283,21 +300,29 @@ export class TestShell {
283300
this._process.kill(signal);
284301
}
285302

286-
writeInput(chars: string, { end = false } = {}): void {
303+
writeInput(
304+
chars: string,
305+
{
306+
end = false,
307+
requireFinishedInitialization = true,
308+
}: TestShellInputOptions = {}
309+
): void {
310+
if (requireFinishedInitialization && !this._initializationKnownToBeFinished)
311+
throw new Error('Wait for shell to be initialized before writing input');
287312
this._process.stdin.write(chars);
288313
if (end) this._process.stdin.end();
289314
}
290315

291-
writeInputLine(chars: string): void {
292-
this.writeInput(`${chars}\n`);
316+
writeInputLine(chars: string, options?: TestShellInputOptions): void {
317+
this.writeInput(`${chars}\n`, options);
293318
}
294319

295320
async executeLine(
296321
line: string,
297-
opts: { timeout?: number; promptPattern?: RegExp } = {}
322+
opts: TestShellWaitForPromptOptions & TestShellInputOptions = {}
298323
): Promise<string> {
299324
const previousOutputLength = this._output.length;
300-
this.writeInputLine(line);
325+
this.writeInputLine(line, opts);
301326
await this.waitForPrompt(previousOutputLength, opts);
302327
return this._output.slice(previousOutputLength);
303328
}

0 commit comments

Comments
 (0)