diff --git a/packages/cli-repl/src/mongosh-repl.spec.ts b/packages/cli-repl/src/mongosh-repl.spec.ts index 4c05a3581..8ac61b058 100644 --- a/packages/cli-repl/src/mongosh-repl.spec.ts +++ b/packages/cli-repl/src/mongosh-repl.spec.ts @@ -23,6 +23,11 @@ import MongoshNodeRepl from './mongosh-repl'; import { parseAnyLogEntry } from '../../shell-api/src/log-entry'; import stripAnsi from 'strip-ansi'; +function nonnull(value: T | null | undefined): NonNullable { + if (!value) throw new Error(); + return value; +} + const delay = promisify(setTimeout); const multilineCode = `(function() { @@ -280,7 +285,7 @@ describe('MongoshNodeRepl', function () { it('handles a long series of errors', async function () { input.write('-asdf();\n'.repeat(20)); await waitEval(bus); - expect(mongoshRepl.runtimeState().repl.listenerCount('SIGINT')).to.equal( + expect(mongoshRepl.runtimeState().repl?.listenerCount('SIGINT')).to.equal( 1 ); }); @@ -548,7 +553,7 @@ describe('MongoshNodeRepl', function () { await tick(); input.write('"bar" })\n'); await tick(); - expect(mongoshRepl.runtimeState().repl.context.obj).to.deep.equal({ + expect(mongoshRepl.runtimeState().context.obj).to.deep.equal({ foo: 'bar', }); expect(output).not.to.include('obj = ({ foo: "bar" })'); @@ -571,7 +576,7 @@ describe('MongoshNodeRepl', function () { await tick(); input.write('\u0004'); // Ctrl+D await tick(); - expect(mongoshRepl.runtimeState().repl.context.obj).to.deep.equal({ + expect(mongoshRepl.runtimeState().context.obj).to.deep.equal({ foo: 'baz', }); expect(output).not.to.include('obj = ({ foo: "baz" })'); @@ -596,7 +601,7 @@ describe('MongoshNodeRepl', function () { await tick(); input.write('"bar" })\n'); await tick(); - expect(mongoshRepl.runtimeState().repl.context.obj).to.deep.equal({ + expect(mongoshRepl.runtimeState().context.obj).to.deep.equal({ foo: 'bar', }); expect(output).not.to.include('obj = ({ foo: "bar" })'); @@ -638,7 +643,10 @@ describe('MongoshNodeRepl', function () { it('does not crash if hitting enter and then up', async function () { input.write('\n'); - await once(mongoshRepl.runtimeState().repl, 'flushHistory'); + await once( + nonnull(mongoshRepl.runtimeState().repl), + 'flushHistory' + ); input.write(`${arrowUp}`); await tick(); }); @@ -646,11 +654,20 @@ describe('MongoshNodeRepl', function () { context('redaction', function () { it('removes sensitive commands by default', async function () { input.write('connect\n'); - await once(mongoshRepl.runtimeState().repl, 'flushHistory'); + await once( + nonnull(mongoshRepl.runtimeState().repl), + 'flushHistory' + ); input.write('connection\n'); - await once(mongoshRepl.runtimeState().repl, 'flushHistory'); + await once( + nonnull(mongoshRepl.runtimeState().repl), + 'flushHistory' + ); input.write('db.test.insert({ email: "foo@example.org" })\n'); - await once(mongoshRepl.runtimeState().repl, 'flushHistory'); + await once( + nonnull(mongoshRepl.runtimeState().repl), + 'flushHistory' + ); expect(getHistory()).to.deep.equal([ 'db.test.insert({ email: "foo@example.org" })', @@ -662,11 +679,20 @@ describe('MongoshNodeRepl', function () { input.write('config.set("redactHistory", "keep");\n'); await tick(); input.write('connect\n'); - await once(mongoshRepl.runtimeState().repl, 'flushHistory'); + await once( + nonnull(mongoshRepl.runtimeState().repl), + 'flushHistory' + ); input.write('connection\n'); - await once(mongoshRepl.runtimeState().repl, 'flushHistory'); + await once( + nonnull(mongoshRepl.runtimeState().repl), + 'flushHistory' + ); input.write('db.test.insert({ email: "foo@example.org" })\n'); - await once(mongoshRepl.runtimeState().repl, 'flushHistory'); + await once( + nonnull(mongoshRepl.runtimeState().repl), + 'flushHistory' + ); expect(getHistory()).to.deep.equal([ 'db.test.insert({ email: "foo@example.org" })', @@ -680,11 +706,20 @@ describe('MongoshNodeRepl', function () { input.write('config.set("redactHistory", "remove-redact");\n'); await tick(); input.write('connect\n'); - await once(mongoshRepl.runtimeState().repl, 'flushHistory'); + await once( + nonnull(mongoshRepl.runtimeState().repl), + 'flushHistory' + ); input.write('connection\n'); - await once(mongoshRepl.runtimeState().repl, 'flushHistory'); + await once( + nonnull(mongoshRepl.runtimeState().repl), + 'flushHistory' + ); input.write('db.test.insert({ email: "foo@example.org" })\n'); - await once(mongoshRepl.runtimeState().repl, 'flushHistory'); + await once( + nonnull(mongoshRepl.runtimeState().repl), + 'flushHistory' + ); expect(getHistory()).to.deep.equal([ 'db.test.insert({ email: "" })', @@ -783,11 +818,9 @@ describe('MongoshNodeRepl', function () { it('does not refresh the prompt if a window resize occurs while evaluating', async function () { let resolveInProgress; - mongoshRepl.runtimeState().repl.context.inProgress = new Promise( - (resolve) => { - resolveInProgress = resolve; - } - ); + mongoshRepl.runtimeState().context.inProgress = new Promise((resolve) => { + resolveInProgress = resolve; + }); input.write('inProgress\n'); await tick(); @@ -887,7 +920,7 @@ describe('MongoshNodeRepl', function () { context('user prompts', function () { beforeEach(function () { // No boolean equivalent for 'passwordPrompt' in the API, so provide one: - mongoshRepl.runtimeState().repl.context.booleanPrompt = (question) => { + mongoshRepl.runtimeState().context.booleanPrompt = (question) => { return Object.assign(mongoshRepl.onPrompt(question, 'yesno'), { [Symbol.for('@@mongosh.syntheticPromise')]: true, }); @@ -1283,6 +1316,29 @@ describe('MongoshNodeRepl', function () { expect(output).to.contain('> '); }); }); + + context('pre-specified user-provided prompt', function () { + it('does not attempt to run the prompt in parallel with initial input', async function () { + output = ''; + const initialized = await mongoshRepl.initialize(serviceProvider); + await mongoshRepl.loadExternalCode( + `prompt = () => { + if (globalThis.isEvaluating) print('FAILED -- Parallel execution detected!'); + globalThis.isEvaluating = true; + sleep(100); + globalThis.isEvaluating = false; + return '> '; + };`, + 'test' + ); + expect(mongoshRepl.runtimeState().context.prompt).to.be.a('function'); + // Queue up input *before* starting the REPL itself + input.write('prompt()\n'); + await mongoshRepl.startRepl(initialized); + await waitEval(bus); + expect(output).not.to.include('FAILED'); + }); + }); }); context('before the REPL starts', function () { diff --git a/packages/cli-repl/src/mongosh-repl.ts b/packages/cli-repl/src/mongosh-repl.ts index 325f2eaed..baa8ce0e7 100644 --- a/packages/cli-repl/src/mongosh-repl.ts +++ b/packages/cli-repl/src/mongosh-repl.ts @@ -533,11 +533,13 @@ class MongoshNodeRepl implements EvaluationListener { 'Cannot start REPL when not in REPL evaluation mode' ); } + // Set up the prompt before consuming input so that we do not end up + // running the prompt function in parallel with actual input code. + repl.setPrompt(await this.getShellPrompt()); // Only start reading from the input *after* we set up everything, including - // instanceState.setCtx(). + // instanceState.setCtx() and configuring the REPL prompt. this.lineByLineInput.start(); this.input.resume(); - repl.setPrompt(await this.getShellPrompt()); repl.displayPrompt(); } diff --git a/packages/e2e-tests/test/e2e.spec.ts b/packages/e2e-tests/test/e2e.spec.ts index 649369260..9f6bfdcc3 100644 --- a/packages/e2e-tests/test/e2e.spec.ts +++ b/packages/e2e-tests/test/e2e.spec.ts @@ -262,6 +262,26 @@ describe('e2e', function () { } expect(buffer).to.include('"i": 99999'); }); + it('handles custom prompt() function in conjunction with line-by-line input well', async function () { + // https://jira.mongodb.org/browse/MONGOSH-1617 + shell = TestShell.start({ + args: [ + '--nodb', + '--shell', + '--eval', + 'prompt = () => {sleep(1);return "x>"}', + ], + }); + // The number of newlines here matters + shell.writeInput( + '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', + { end: true } + ); + const exitCode = await shell.waitForExit(); + expect(exitCode).to.equal(0); + shell.assertContainsOutput('3628800'); + shell.assertNoErrors(); + }); }); describe('set db', function () { @@ -1035,6 +1055,25 @@ describe('e2e', function () { shell.assertContainsOutput('admin;system.version;'); }); }); + + it('works fine with custom prompts', async function () { + // https://jira.mongodb.org/browse/MONGOSH-1617 + shell = TestShell.start({ + args: [ + await testServer.connectionString(), + '--eval', + 'prompt = () => db.stats().db', + '--shell', + ], + }); + shell.writeInput( + '[db.hello()].reduce(\n() => { return 11111*11111; },0)\n\n\n', + { end: true } + ); + await shell.waitForExit(); + shell.assertContainsOutput('123454321'); + shell.assertNoErrors(); + }); }); describe('Node.js builtin APIs in the shell', function () { diff --git a/packages/e2e-tests/test/test-shell.ts b/packages/e2e-tests/test/test-shell.ts index a274e1b29..9cf384b88 100644 --- a/packages/e2e-tests/test/test-shell.ts +++ b/packages/e2e-tests/test/test-shell.ts @@ -215,8 +215,9 @@ export class TestShell { this._process.kill(signal); } - writeInput(chars: string): void { + writeInput(chars: string, { end = false } = {}): void { this._process.stdin.write(chars); + if (end) this._process.stdin.end(); } writeInputLine(chars: string): void {