Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions packages/cli-repl/src/arg-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const OPTIONS = {
boolean: [
'async',
'help',
'internalTestCommands',
'ipv6',
'nodb',
'norc',
Expand Down Expand Up @@ -87,14 +88,15 @@ function getLocale(args: string[], env: any): string {
return lang ? lang.split('.')[0] : lang;
}

type AllCliOptions = CliOptions & { smokeTests: boolean, internalTestCommands: boolean };
/**
* Parses arguments into a JS object.
*
* @param {string[]} args - The args.
*
* @returns {CliOptions} The arguments as cli options.
*/
function parse(args: string[]): (CliOptions & { smokeTests: boolean }) {
function parse(args: string[]): AllCliOptions {
const programArgs = args.slice(2);
i18n.setLocale(getLocale(programArgs, process.env));

Expand All @@ -111,7 +113,7 @@ function parse(args: string[]): (CliOptions & { smokeTests: boolean }) {
${USAGE}`
);
});
return parsed as unknown as (CliOptions & { smokeTests: boolean });
return parsed as unknown as AllCliOptions;
}

export default parse;
Expand Down
20 changes: 20 additions & 0 deletions packages/cli-repl/src/async-repl.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { once } from 'events';
import chai, { expect } from 'chai';
import sinon from 'ts-sinon';
import sinonChai from 'sinon-chai';
import { tick } from '../test/repl-helpers';
chai.use(sinonChai);

const delay = promisify(setTimeout);
Expand Down Expand Up @@ -123,6 +124,25 @@ describe('AsyncRepl', () => {
expect(foundUid).to.be.true;
});

it('delays the "exit" event until after asynchronous evaluation is finished', async() => {
const { input, repl } = createDefaultAsyncRepl();
let exited = false;
repl.on('exit', () => { exited = true; });

let resolve;
repl.context.asyncFn = () => new Promise((res) => { resolve = res; });

input.end('asyncFn()\n');
expect(exited).to.be.false;

await tick();
resolve();
expect(exited).to.be.false;

await tick();
expect(exited).to.be.true;
});

describe('allows handling exceptions from e.g. the writer function', () => {
it('for succesful completions', async() => {
const error = new Error('throwme');
Expand Down
20 changes: 20 additions & 0 deletions packages/cli-repl/src/async-repl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,13 @@ export function start(opts: AsyncREPLOptions): REPLServer {
repl.emit(evalStart, { input } as EvalStartEvent);

try {
let exitEventPending = false;
const exitListener = () => { exitEventPending = true; };
let previousExitListeners: any[] = [];

let sigintListener: (() => void) | undefined = undefined;
let previousSigintListeners: any[] = [];

try {
result = await new Promise((resolve, reject) => {
if (breakEvalOnSigint) {
Expand All @@ -68,6 +73,13 @@ export function start(opts: AsyncREPLOptions): REPLServer {
repl.once('SIGINT', sigintListener);
}

// The REPL may become over-eager and emit 'exit' events while our
// evaluation is still in progress (because it doesn't expect async
// evaluation). If that happens, defer the event until later.
previousExitListeners = repl.rawListeners('exit');
repl.removeAllListeners('exit');
repl.once('exit', exitListener);

const evalResult = asyncEval(originalEval, input, context, filename);

if (sigintListener !== undefined) {
Expand All @@ -84,6 +96,14 @@ export function start(opts: AsyncREPLOptions): REPLServer {
for (const listener of previousSigintListeners) {
repl.on('SIGINT', listener);
}

repl.removeListener('exit', exitListener);
for (const listener of previousExitListeners) {
repl.on('exit', listener);
}
if (exitEventPending) {
process.nextTick(() => repl.emit('exit'));
}
}
} catch (err) {
try {
Expand Down
9 changes: 9 additions & 0 deletions packages/cli-repl/src/cli-repl.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,7 @@ describe('CliRepl', () => {
await once(srv, 'listening');
host = `http://localhost:${(srv.address() as any).port}`;
cliReplOptions.analyticsOptions = { host, apiKey: '🔑' };
cliReplOptions.shellCliOptions.internalTestCommands = true;
cliRepl = new CliRepl(cliReplOptions);
await cliRepl.start(await testServer.connectionString(), {});
});
Expand Down Expand Up @@ -417,6 +418,14 @@ describe('CliRepl', () => {
req => JSON.parse(req.body).batch.filter(entry => entry.event === 'Use')).flat();
expect(useEvents).to.have.lengthOf(2);
});

it('can self-test the analytics implementation', async() => {
input.write('__verifyAnalytics();\n');
await waitEval(cliRepl.bus);
const selfTestEvents = requests.map(
req => JSON.parse(req.body).batch.filter(entry => entry.event === '__verifyAnalytics')).flat();
expect(selfTestEvents).to.have.lengthOf(1);
});
});

context('without network connectivity', () => {
Expand Down
16 changes: 14 additions & 2 deletions packages/cli-repl/src/cli-repl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const CONNECTING = 'cli-repl.cli-repl.connecting';
type AnalyticsOptions = { host?: string, apiKey?: string };

export type CliReplOptions = {
shellCliOptions: CliOptions & { mongocryptdSpawnPath?: string },
shellCliOptions: CliOptions & { mongocryptdSpawnPath?: string, internalTestCommands?: boolean },
input: Readable;
output: Writable;
shellHomePaths: ShellHomePaths;
Expand All @@ -41,7 +41,7 @@ export type CliReplOptions = {
class CliRepl {
mongoshRepl: MongoshNodeRepl;
bus: MongoshBus;
cliOptions: CliOptions;
cliOptions: CliOptions & { internalTestCommands?: boolean };
shellHomeDirectory: ShellHomeDirectory;
configDirectory: ConfigManager<UserConfig>;
config: UserConfig = new UserConfig();
Expand Down Expand Up @@ -140,6 +140,18 @@ class CliRepl {

const initialServiceProvider = await this.connect(driverUri, driverOptions);
await this.mongoshRepl.start(initialServiceProvider);

if (this.analytics && this.cliOptions.internalTestCommands) {
const analytics = this.analytics;
this.mongoshRepl.context.__verifyAnalytics = async() => {
const promise = promisify(analytics.track.bind(analytics))({
userId: this.config.userId, event: '__verifyAnalytics'
});
analytics.flush();
await promise;
return 'API call succeeded';
};
}
}

/**
Expand Down
6 changes: 5 additions & 1 deletion packages/cli-repl/src/mongosh-repl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ class MongoshNodeRepl implements EvaluationListener {
} catch { /* ... */ }
});

internalState.setCtx(repl.context);
internalState.setCtx(this.context);
// Only start reading from the input *after* we set up everything, including
// internalState.setCtx().
this.lineByLineInput.start();
Expand Down Expand Up @@ -387,6 +387,10 @@ class MongoshNodeRepl implements EvaluationListener {
return this._runtimeState;
}

get context(): any {
return this.runtimeState().repl.context;
}

async close(): Promise<void> {
const rs = this._runtimeState;
if (rs) {
Expand Down
8 changes: 7 additions & 1 deletion packages/cli-repl/src/smoke-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export async function runSmokeTests(smokeTestServer: string | undefined, executa
assert(!!smokeTestServer, 'Make sure MONGOSH_SMOKE_TEST_SERVER is set in CI');
}

let n = 0;
for (const { input, output, testArgs } of [{
input: 'print("He" + "llo" + " Wor" + "ld!")',
output: /Hello World!/,
Expand All @@ -22,10 +23,15 @@ export async function runSmokeTests(smokeTestServer: string | undefined, executa
input: fleSmokeTestScript,
output: /Test succeeded|Test skipped/,
testArgs: [smokeTestServer as string]
}] : []).concat(process.execPath === process.argv[1] ? [{
input: '__verifyAnalytics()',
output: /API call succeeded/,
testArgs: ['--internalTestCommands', '--nodb']
}] : [])) {
n++;
await runSmokeTest(executable, [...args, ...testArgs], input, output);
}
console.log('all tests passed');
console.log(`${n} tests passed!`);
}

async function runSmokeTest(executable: string, args: string[], input: string, output: RegExp): Promise<void> {
Expand Down