Better shell error handling, add mongo.shell.args setting (#1162)

* Better shell error handling, mongo.shell.args setting

* remove comment

* PR suggestion

* PR fixes
This commit is contained in:
Stephen Weatherford (MSFT) 2019-08-22 15:43:56 -07:00 коммит произвёл GitHub
Родитель 04626fb9f3
Коммит abea51af1a
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
24 изменённых файлов: 941 добавлений и 235 удалений

3
.gitignore поставляемый
Просмотреть файл

@ -266,3 +266,6 @@ dist
stats.json
*.tgz
*.zip
# Scrapbooks
*.mongo

41
.vscode/launch.json поставляемый
Просмотреть файл

@ -23,26 +23,6 @@
"NODE_DEBUG": ""
}
},
{
"name": "Launch Extension (Webpack)",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}"
],
"stopOnEntry": false,
"sourceMaps": true,
// outFiles is used for locating generated JavaScript files, see https://code.visualstudio.com/docs/nodejs/nodejs-debugging#_source-maps
"outFiles": [
"${workspaceFolder}/dist/**/*.js"
],
"preLaunchTask": "npm: webpack",
"env": {
"DEBUGTELEMETRY": "1",
"NODE_DEBUG": ""
}
},
{
"name": "Launch Tests",
"type": "extensionHost",
@ -60,6 +40,7 @@
],
"preLaunchTask": "npm: compile",
"env": {
"MOCHA_fgrep": "", // Search string of tests to run (empty for all)
"MOCHA_grep": "", // RegExp of tests to run (empty for all)
"MOCHA_enableTimeouts": "0", // Disable time-outs
"AZCODE_COSMOSDB_IGNORE_BUNDLE": "1",
@ -67,6 +48,26 @@
"NODE_DEBUG": ""
}
},
{
"name": "Launch Extension (Webpack)",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}"
],
"stopOnEntry": false,
"sourceMaps": true,
// outFiles is used for locating generated JavaScript files, see https://code.visualstudio.com/docs/nodejs/nodejs-debugging#_source-maps
"outFiles": [
"${workspaceFolder}/dist/**/*.js"
],
"preLaunchTask": "npm: webpack",
"env": {
"DEBUGTELEMETRY": "1",
"NODE_DEBUG": ""
}
},
{
"name": "Launch Tests (Webpack)",
"type": "extensionHost",

Просмотреть файл

@ -24,3 +24,4 @@ grammar/*.g4
build
*.ts
*.zip
*.mongo

Просмотреть файл

@ -27,6 +27,12 @@ export { addDatabaseToAccountConnectionString, getDatabaseNameFromConnectionStri
export { MongoCommand } from './src/mongo/MongoCommand';
export { getAllCommandsFromText, getCommandFromTextAtLocation } from './src/mongo/MongoScrapbook';
export { rejectOnTimeout, valueOnTimeout } from './src/utils/timeout';
export { improveError } from './src/utils/improveError';
export { parseError } from 'vscode-azureextensionui';
export { MongoShell } from './src/mongo/MongoShell';
export { wrapError } from './src/utils/wrapError';
export { isWindows } from './src/constants';
export { IDisposable } from './src/utils/vscodeUtils';
// The tests use instanceof against these and therefore we need to make sure we're using the same version of the bson module in the tests as in the bundle,
// so export it from the bundle itself.

12
package-lock.json сгенерированный
Просмотреть файл

@ -2679,9 +2679,9 @@
}
},
"diagnostic-channel-publishers": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/diagnostic-channel-publishers/-/diagnostic-channel-publishers-0.3.2.tgz",
"integrity": "sha512-2hBlg1BtBT+nd04MGGGZinDv5gOTRQOCzdgk+KRQZ20XJ/uepC0B0rwWLQtz6Tk6InXymWqsk1sMC975cPEReA=="
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/diagnostic-channel-publishers/-/diagnostic-channel-publishers-0.3.3.tgz",
"integrity": "sha512-qIocRYU5TrGUkBlDDxaziAK1+squ8Yf2Ls4HldL3xxb/jzmWO2Enux7CvevNKYmF2kDXZ9HiRqwjPsjk8L+i2Q=="
},
"diff": {
"version": "3.5.0",
@ -9752,9 +9752,9 @@
}
},
"vscode-azureextensionui": {
"version": "0.26.3",
"resolved": "https://registry.npmjs.org/vscode-azureextensionui/-/vscode-azureextensionui-0.26.3.tgz",
"integrity": "sha512-bWWj7S4+PSAFgwoHUcZSnjo0s7qLNAHxn/yY4EbvyzjPff83BgMe6Mkla+h0F7l33Pfc75VZoZKl3v6kNNzwcA==",
"version": "0.26.5",
"resolved": "https://registry.npmjs.org/vscode-azureextensionui/-/vscode-azureextensionui-0.26.5.tgz",
"integrity": "sha512-e1JV7DCVSgPqU782jh/o0QLSloCNUoIr/BxZyq61NF7C+9KeOkzQAAADlKV53rExVf/G5irDgIJyO4JAThgrsA==",
"requires": {
"azure-arm-resource": "^3.0.0-preview",
"azure-arm-storage": "^3.1.0",

Просмотреть файл

@ -740,9 +740,19 @@
"string",
"null"
],
"description": "Command to execute or full path to folder and executable to start the Mongo shell, needed by some Mongo scrapbook commands. If empty, will search in the system path for 'mongo'.",
"description": "Full path to folder and executable to start the Mongo shell, needed by some Mongo scrapbook commands. If empty, will search in the system path for 'mongo'.",
"default": null
},
"mongo.shell.args": {
"type": "array",
"items": {
"type": "string"
},
"description": "Arguments to pass when starting the Mongo shell.",
"default": [
"--quiet"
]
},
"mongo.shell.timeout": {
"type": "number",
"description": "The duration allowed (in seconds) for the Mongo shell to execute a command. Default value is 5 seconds",
@ -924,7 +934,7 @@
"socket.io": "^1.7.3",
"socket.io-client": "^1.7.3",
"underscore": "^1.8.3",
"vscode-azureextensionui": "^0.26.3",
"vscode-azureextensionui": "0.26.5",
"vscode-json-languageservice": "^3.0.8",
"vscode-languageclient": "^4.4.0",
"vscode-languageserver": "^4.4.0",

Просмотреть файл

@ -3,6 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { NewDocument } from 'documentdb';
import * as fse from 'fs-extra';
import * as vscode from 'vscode';
@ -43,7 +44,9 @@ export async function importDocuments(actionContext: IActionContext, uris: vscod
const documents = await parseDocuments(uris);
progress.report({ increment: 30, message: "Parsed documents. Importing" });
if (collectionNode instanceof MongoCollectionTreeItem) {
result = processMongoResults(await collectionNode.executeCommand('insertMany', [JSON.stringify(documents)]));
let { deferToShell, result: tryExecuteResult } = await collectionNode.tryExecuteCommandDirectly('insertMany', [JSON.stringify(documents)]);
assert(!deferToShell, "This command should not need to be sent to the shell");
result = processMongoResults(tryExecuteResult);
} else {
result = await insertDocumentsIntoDocdb(collectionNode, documents, uris);
}

Просмотреть файл

@ -3,6 +3,8 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export const isWindows: boolean = /^win/.test(process.platform);
import * as assert from 'assert';
import * as fs from 'fs';
import * as path from 'path';

Просмотреть файл

@ -25,6 +25,7 @@ export namespace ext {
export namespace settingsKeys {
export const mongoShellPath = 'mongo.shell.path';
export const mongoShellArgs = 'mongo.shell.args';
export const documentLabelFields = 'cosmosDB.documentLabelFields';
export const mongoShellTimeout = 'mongo.shell.timeout';

Просмотреть файл

@ -42,7 +42,7 @@ export function getAllErrorsFromTextDocument(document: vscode.TextDocument): vsc
}
export async function executeAllCommandsFromActiveEditor(database: MongoDatabaseTreeItem, editorManager: CosmosEditorManager, context: IActionContext): Promise<void> {
ext.outputChannel.appendLine("Running all commands in scrapbook...");
ext.outputChannel.appendLine("Executing all commands in scrapbook...");
let commands = getAllCommandsFromActiveEditor();
await executeCommands(vscode.window.activeTextEditor, database, editorManager, context, commands);
}
@ -94,7 +94,7 @@ async function executeCommands(activeEditor: vscode.TextEditor, database: MongoD
async function executeCommand(activeEditor: vscode.TextEditor, database: MongoDatabaseTreeItem, editorManager: CosmosEditorManager, context: IActionContext, command: MongoCommand): Promise<void> {
if (command) {
ext.outputChannel.appendLine(command.text);
ext.outputChannel.appendLine(`Executing command: ${command.text}`);
try {
context.telemetry.properties["command"] = command.name;

181
src/mongo/MongoShell.ts Normal file
Просмотреть файл

@ -0,0 +1,181 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as os from 'os';
import * as vscode from 'vscode';
import { parseError } from 'vscode-azureextensionui';
import { InteractiveChildProcess } from '../utils/InteractiveChildProcess';
import { randomUtils } from '../utils/randomUtils';
import { wrapError } from '../utils/wrapError';
const timeoutMessage = "Timed out trying to execute the Mongo script. To use a longer timeout, modify the VS Code 'mongo.shell.timeout' setting.";
const mongoShellMoreMessage = 'Type "it" for more';
const extensionMoreMessage = '(More)';
const sentinelBase = 'EXECUTION COMPLETED';
const sentinelRegex = /\"?EXECUTION COMPLETED [0-9a-fA-F]{10}\"?/;
function createSentinel(): string { return `${sentinelBase} ${randomUtils.getRandomHexString(10)}`; }
export class MongoShell extends vscode.Disposable {
public static async create(execPath: string, execArgs: string[], connectionString: string, isEmulator: boolean, outputChannel: vscode.OutputChannel, timeoutSeconds: number): Promise<MongoShell> {
try {
let args: string[] = execArgs.slice() || []; // Snapshot since we modify it
args.push(connectionString);
if (isEmulator) {
// Without these the connection will fail due to the self-signed DocDB certificate
if (args.indexOf("--ssl") < 0) {
args.push("--ssl");
}
if (args.indexOf("--sslAllowInvalidCertificates") < 0) {
args.push("--sslAllowInvalidCertificates");
}
}
let process: InteractiveChildProcess = await InteractiveChildProcess.create({
outputChannel: outputChannel,
command: execPath,
args,
outputFilterSearch: sentinelRegex,
outputFilterReplace: ''
});
let shell: MongoShell = new MongoShell(process, timeoutSeconds);
// Try writing an empty script to verify the process is running correctly and allow us
// to catch any errors related to the start-up of the process before trying to write to it.
await shell.executeScript("");
return shell;
} catch (error) {
throw wrapCheckOutputWindow(error);
}
}
constructor(private _process: InteractiveChildProcess, private _timeoutSeconds: number) {
super(() => this.dispose());
}
public dispose(): void {
this._process.kill();
}
public async useDatabase(database: string): Promise<string> {
return await this.executeScript(`use ${database}`);
}
public async executeScript(script: string): Promise<string> {
script = convertToSingleLine(script);
let stdOut = "";
const sentinel = createSentinel();
let disposables: vscode.Disposable[] = [];
try {
let result = await new Promise<string>(async (resolve, reject) => {
try {
startScriptTimeout(this._timeoutSeconds, reject);
// Hook up events
disposables.push(
this._process.onStdOut(text => {
stdOut += text;
let { text: stdOutNoSentinel, removed } = removeSentinel(stdOut, sentinel);
if (removed) {
// The sentinel was found, which means we are done.
// Change the "type 'it' for more" message to one that doesn't ask users to type anything,
// since we're not currently interactive like that.
// CONSIDER: Ideally we would allow users to click a button to iterate through more data,
// or even just do it for them
stdOutNoSentinel = stdOutNoSentinel.replace(mongoShellMoreMessage, extensionMoreMessage);
resolve(stdOutNoSentinel);
}
}));
disposables.push(
this._process.onStdErr(text => {
// Mongo shell only writes to STDERR for errors relating to starting up. Script errors go to STDOUT.
// So consider this an error.
// (It's okay if we fire this multiple times, the first one wins.)
reject(wrapCheckOutputWindow(text.trim()));
}));
disposables.push(
this._process.onError(error => {
reject(error);
}));
// Write the script to STDIN
if (script) {
this._process.writeLine(script);
}
// Mark end of result by sending the sentinel wrapped in quotes so the console will spit
// it back out as a string value after it's done processing the script
let quotedSentinel = `"${sentinel}"`;
this._process.writeLine(quotedSentinel); // (Don't display the sentinel)
} catch (error) {
// new Promise() doesn't seem to catch exceptions in an async function, we need to explicitly reject it
if ((<{ code?: string }>error).code === 'EPIPE') {
// Give a chance for start-up errors to show up before rejecting with this more general error message
await delay(500);
error = new Error("The process exited prematurely.");
}
reject(wrapCheckOutputWindow(error));
}
});
return result.trim();
}
finally {
// Dispose event handlers
for (let d of disposables) {
d.dispose();
}
}
}
}
function startScriptTimeout(timeoutSeconds: number | 0, reject: (unknown) => void): void {
if (timeoutSeconds > 0) {
setTimeout(
() => {
reject(timeoutMessage);
},
timeoutSeconds * 1000);
}
}
function convertToSingleLine(script: string): string {
return script.split(os.EOL)
.map(line => line.trim())
.join('');
}
function removeSentinel(text: string, sentinel: string): { text: string; removed: boolean } {
let index = text.indexOf(sentinel);
if (index >= 0) {
return { text: text.slice(0, index), removed: true };
} else {
return { text, removed: false };
}
}
async function delay(milliseconds: number): Promise<void> {
return new Promise(resolve => {
// tslint:disable-next-line:no-string-based-set-timeout // false positive
setTimeout(resolve, milliseconds);
});
}
function wrapCheckOutputWindow(error: unknown): unknown {
let checkOutputMsg = "The output window may contain additional information.";
return parseError(error).message.includes(checkOutputMsg) ? error : wrapError(error, checkOutputMsg);
}

Просмотреть файл

@ -39,7 +39,7 @@ export class MongoFindOneResultEditor implements ICosmosEditor<IMongoDocument> {
node.refresh();
} else {
// If the node isn't cached already, just update it to Mongo directly (without worrying about updating the tree)
const db = await this._databaseNode.getDb();
const db = await this._databaseNode.connectToDb();
result = await MongoDocumentTreeItem.update(db.collection(this._collectionName), newDocument);
}
return result;

Просмотреть файл

@ -31,7 +31,7 @@ export class MongoFindResultEditor implements ICosmosEditor<IMongoDocument[]> {
}
public async getData(context: IActionContext): Promise<IMongoDocument[]> {
const db = await this._databaseNode.getDb();
const db = await this._databaseNode.connectToDb();
const collection: Collection = db.collection(this._command.collection);
// NOTE: Intentionally creating a _new_ tree item rather than searching for a cached node in the tree because
// the executed 'find' command could have a filter or projection that is not handled by a cached tree node

Просмотреть файл

@ -1,143 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as cp from 'child_process';
import * as os from 'os';
import * as vscode from "vscode";
import { EventEmitter, window } from 'vscode';
import { ext } from '../extensionVariables';
import { IDisposable, toDisposable } from '../utils/vscodeUtils';
// This is used at the end of each command we send to the console. When we get this string back,
// we know we've reached the end of that command's result.
const endOfDataSentinelBase: string = '$EOD$';
export class Shell {
private executionId: number = 0;
private disposables: IDisposable[] = [];
private onResult: EventEmitter<{ exitCode, result, stderr, code?: string, message?: string }> = new EventEmitter<{ exitCode, result, stderr, code?: string, message?: string }>();
public static create(execPath: string, connectionString: string, isEmulator: boolean): Promise<Shell> {
return new Promise((c, e) => {
try {
let args = ['--quiet', connectionString];
if (isEmulator) {
// Without this the connection will fail due to the self-signed DocDB certificate
args.push("--ssl");
args.push("--sslAllowInvalidCertificates");
}
const shellProcess = cp.spawn(execPath, args);
return c(new Shell(execPath, shellProcess));
} catch (error) {
e(`Error while creating mongo shell with path '${execPath}': ${error}`);
}
});
}
constructor(private execPath: string, private mongoShell: cp.ChildProcess) {
this.initialize();
}
private initialize() {
const once = (ee: NodeJS.EventEmitter, name: string, fn: Function) => {
ee.once(name, fn);
this.disposables.push(toDisposable(() => ee.removeListener(name, fn)));
};
const on = (ee: NodeJS.EventEmitter, name: string, fn: Function) => {
ee.on(name, fn);
this.disposables.push(toDisposable(() => ee.removeListener(name, fn)));
};
once(this.mongoShell, 'error', result => this.onResult.fire(result));
once(this.mongoShell, 'exit', result => this.onResult.fire(result));
let buffers: string[] = [];
on(this.mongoShell.stdout, 'data', b => {
let data: string = b.toString();
const endOfDataSentinel = `${endOfDataSentinelBase}${this.executionId}${os.EOL}`;
if (data.endsWith(endOfDataSentinel)) {
const result = buffers.join('') + data.substring(0, data.length - endOfDataSentinel.length);
buffers = [];
this.onResult.fire({
exitCode: void 0,
result,
stderr: void 0
});
} else {
buffers.push(b);
}
});
on(this.mongoShell.stderr, 'data', result => this.onResult.fire(result));
once(this.mongoShell.stderr, 'close', result => this.onResult.fire(result));
}
async useDatabase(database: string): Promise<string> {
return this.exec(`use ${database}`);
}
async exec(script: string): Promise<string> {
script = this.convertToSingleLine(script);
const executionId = this._generateExecutionSequenceId();
try {
this.mongoShell.stdin.write(script, 'utf8');
this.mongoShell.stdin.write(os.EOL);
// Mark end of result by sending the sentinel wrapped in quotes so the console will spit
// it back out as a string value
this.mongoShell.stdin.write(`"${endOfDataSentinelBase}${executionId}"`, 'utf8');
this.mongoShell.stdin.write(os.EOL);
} catch (error) {
window.showErrorMessage(error.toString());
}
return await new Promise<string>((c, e) => {
let executed = false;
// timeout setting specified in seconds. Convert to ms for setTimeout
const timeout: number = 1000 * vscode.workspace.getConfiguration().get<number>(ext.settingsKeys.mongoShellTimeout);
const handler = setTimeout(
() => {
if (!executed) {
e(`Timed out executing MongoDB command "${script}"`);
}
},
timeout);
const disposable = this.onResult.event(result => {
disposable.dispose();
if (result && result.code) {
if (result.code === 'ENOENT') {
result.message = `This functionality requires the Mongo DB shell, but we could not find it. Please make sure it is on your path or you have set the '${ext.settingsKeys.mongoShellPath}' VS Code setting to point to the Mongo shell executable file (not folder). Attempted command: "${this.execPath}"`;
}
e(result);
} else {
let lines = (<string>result.result).split(os.EOL).filter(line => !!line && line !== 'Type "it" for more');
lines = lines[lines.length - 1] === 'Type "it" for more' ? lines.splice(lines.length - 1, 1) : lines;
executed = true;
c(lines.join(os.EOL));
}
if (handler) {
clearTimeout(handler);
}
});
});
}
private convertToSingleLine(script: string): string {
return script.split(os.EOL)
.map(line => line.trim())
.join('')
.trim();
}
private _generateExecutionSequenceId(): string {
return `${++this.executionId}`;
}
}

Просмотреть файл

@ -107,41 +107,36 @@ export class MongoCollectionTreeItem extends AzureParentTreeItem<IMongoTreeRoot>
return new MongoDocumentTreeItem(this, newDocument);
}
executeCommand(name: string, args?: string[]): Thenable<string> | null {
public async tryExecuteCommandDirectly(name: string, args?: string[]): Promise<{ deferToShell: true; result: undefined } | { deferToShell: false; result: string }> {
const parameters = args ? args.map(parseJSContent) : undefined;
const deferToShell = null; //The value executeCommand returns to instruct the caller function to run the same command in the Mongo shell.
try {
const functions = {
drop: new FunctionDescriptor(this.drop, 'Dropping collection', 0, 0, 0),
count: new FunctionDescriptor(this.count, 'Counting documents', 0, 2, 2),
findOne: new FunctionDescriptor(this.findOne, 'Finding document', 0, 2, 2),
insert: new FunctionDescriptor(this.insert, 'Inserting document', 1, 1, 1),
insertMany: new FunctionDescriptor(this.insertMany, 'Inserting documents', 1, 2, 2),
insertOne: new FunctionDescriptor(this.insertOne, 'Inserting document', 1, 2, 2),
deleteMany: new FunctionDescriptor(this.deleteMany, 'Deleting documents', 1, 2, 1),
deleteOne: new FunctionDescriptor(this.deleteOne, 'Deleting document', 1, 2, 1),
remove: new FunctionDescriptor(this.remove, 'Deleting document(s)', 1, 2, 1)
};
const functions = {
drop: new FunctionDescriptor(this.drop, 'Dropping collection', 0, 0, 0),
count: new FunctionDescriptor(this.count, 'Counting documents', 0, 2, 2),
findOne: new FunctionDescriptor(this.findOne, 'Finding document', 0, 2, 2),
insert: new FunctionDescriptor(this.insert, 'Inserting document', 1, 1, 1),
insertMany: new FunctionDescriptor(this.insertMany, 'Inserting documents', 1, 2, 2),
insertOne: new FunctionDescriptor(this.insertOne, 'Inserting document', 1, 2, 2),
deleteMany: new FunctionDescriptor(this.deleteMany, 'Deleting documents', 1, 2, 1),
deleteOne: new FunctionDescriptor(this.deleteOne, 'Deleting document', 1, 2, 1),
remove: new FunctionDescriptor(this.remove, 'Deleting document(s)', 1, 2, 1)
};
if (functions.hasOwnProperty(name)) {
let descriptor: FunctionDescriptor = functions[name];
if (functions.hasOwnProperty(name)) {
let descriptor: FunctionDescriptor = functions[name];
if (parameters.length < descriptor.minShellArgs) {
return Promise.reject(new Error(`Too few arguments passed to command ${name}.`));
}
if (parameters.length > descriptor.maxShellArgs) {
return Promise.reject(new Error(`Too many arguments passed to command ${name}`));
}
if (parameters.length > descriptor.maxHandledArgs) { //this function won't handle these arguments, but the shell will
return deferToShell;
}
return reportProgress(descriptor.mongoFunction.apply(this, parameters), descriptor.text);
if (parameters.length < descriptor.minShellArgs) {
throw new Error(`Too few arguments passed to command ${name}.`);
}
return deferToShell;
} catch (error) {
return Promise.reject(error);
if (parameters.length > descriptor.maxShellArgs) {
throw new Error(`Too many arguments passed to command ${name}`);
}
if (parameters.length > descriptor.maxHandledArgs) { //this function won't handle these arguments, but the shell will
return { deferToShell: true, result: undefined };
}
return await reportProgress(descriptor.mongoFunction.apply(this, parameters), descriptor.text);
}
return { deferToShell: true, result: undefined };
}
public async deleteTreeItemImpl(): Promise<void> {

Просмотреть файл

@ -15,12 +15,13 @@ import * as cpUtils from '../../utils/cp';
import { connectToMongoClient } from '../connectToMongoClient';
import { MongoCommand } from '../MongoCommand';
import { addDatabaseToAccountConnectionString } from '../mongoConnectionStrings';
import { Shell } from '../shell';
import { MongoShell } from '../MongoShell';
import { IMongoTreeRoot } from './IMongoTreeRoot';
import { MongoAccountTreeItem } from './MongoAccountTreeItem';
import { MongoCollectionTreeItem } from './MongoCollectionTreeItem';
const mongoExecutableFileName = process.platform === 'win32' ? 'mongo.exe' : 'mongo';
const executingInShellMsg = "Executing command in Mongo shell";
export class MongoDatabaseTreeItem extends AzureParentTreeItem<IMongoTreeRoot> {
public static contextValue: string = "mongoDb";
@ -60,7 +61,7 @@ export class MongoDatabaseTreeItem extends AzureParentTreeItem<IMongoTreeRoot> {
}
public async loadMoreChildrenImpl(_clearCache: boolean): Promise<MongoCollectionTreeItem[]> {
const db: Db = await this.getDb();
const db: Db = await this.connectToDb();
const collections: Collection[] = await db.collections();
return collections.map(collection => new MongoCollectionTreeItem(this, collection));
}
@ -81,42 +82,42 @@ export class MongoDatabaseTreeItem extends AzureParentTreeItem<IMongoTreeRoot> {
const message: string = `Are you sure you want to delete database '${this.label}'?`;
const result = await vscode.window.showWarningMessage(message, { modal: true }, DialogResponses.deleteResponse, DialogResponses.cancel);
if (result === DialogResponses.deleteResponse) {
const db = await this.getDb();
const db = await this.connectToDb();
await db.dropDatabase();
} else {
throw new UserCancelledError();
}
}
public async getDb(): Promise<Db> {
public async connectToDb(): Promise<Db> {
const accountConnection = await connectToMongoClient(this.connectionString, appendExtensionUserAgent());
return accountConnection.db(this.databaseName);
}
async executeCommand(command: MongoCommand, context: IActionContext): Promise<string> {
public async executeCommand(command: MongoCommand, context: IActionContext): Promise<string> {
if (command.collection) {
let db = await this.getDb();
let db = await this.connectToDb();
const collection = db.collection(command.collection);
if (collection) {
const collectionTreeItem = new MongoCollectionTreeItem(this, collection, command.arguments);
const result = await collectionTreeItem.executeCommand(command.name, command.arguments);
if (result) {
return result;
const result = await collectionTreeItem.tryExecuteCommandDirectly(command.name, command.arguments);
if (!result.deferToShell) {
return result.result;
}
}
return withProgress(this.executeCommandInShell(command, context), 'Executing command');
return withProgress(this.executeCommandInShell(command, context), executingInShellMsg);
}
if (command.name === 'createCollection') {
return withProgress(this.createCollection(stripQuotes(command.arguments.join(','))).then(() => JSON.stringify({ 'Created': 'Ok' })), 'Creating collection');
} else {
return withProgress(this.executeCommandInShell(command, context), 'Executing command');
return withProgress(this.executeCommandInShell(command, context), executingInShellMsg);
}
}
async createCollection(collectionName: string): Promise<MongoCollectionTreeItem> {
const db: Db = await this.getDb();
public async createCollection(collectionName: string): Promise<MongoCollectionTreeItem> {
const db: Db = await this.connectToDb();
const newCollection: Collection = db.collection(collectionName);
// db.createCollection() doesn't create empty collections for some reason
// However, we can 'insert' and then 'delete' a document, which has the side-effect of creating an empty collection
@ -125,20 +126,33 @@ export class MongoDatabaseTreeItem extends AzureParentTreeItem<IMongoTreeRoot> {
return new MongoCollectionTreeItem(this, newCollection);
}
executeCommandInShell(command: MongoCommand, context: IActionContext): Thenable<string> {
private async executeCommandInShell(command: MongoCommand, context: IActionContext): Promise<string> {
context.telemetry.properties["executeInShell"] = "true";
return this.getShell().then(shell => shell.exec(command.text));
// CONSIDER: Re-using the shell instead of disposing it each time would allow us to keep state
// (JavaScript variables, etc.), but we would need to deal with concurrent requests, or timed-out
// requests.
let shell = await this.createShell();
try {
await shell.useDatabase(this.databaseName);
return await shell.executeScript(command.text);
} finally {
shell.dispose();
}
}
private async getShell(): Promise<Shell> {
let shellPathSetting: string | undefined = vscode.workspace.getConfiguration().get(ext.settingsKeys.mongoShellPath);
if (!this._cachedShellPathOrCmd || this._previousShellPathSetting !== shellPathSetting) {
private async createShell(): Promise<MongoShell> {
let shellPath: string | undefined = vscode.workspace.getConfiguration().get<string>(ext.settingsKeys.mongoShellPath);
let shellArgs: string[] | undefined = vscode.workspace.getConfiguration().get<string[]>(ext.settingsKeys.mongoShellArgs);
if (!this._cachedShellPathOrCmd || this._previousShellPathSetting !== shellPath) {
// Only do this if setting changed since last time
this._previousShellPathSetting = shellPathSetting;
await this._determineShellPathOrCmd(shellPathSetting);
this._previousShellPathSetting = shellPath;
await this._determineShellPathOrCmd(shellPath);
}
return await this.createShell(this._cachedShellPathOrCmd);
let timeout = 1000 * vscode.workspace.getConfiguration().get<number>(ext.settingsKeys.mongoShellTimeout);
return MongoShell.create(shellPath, shellArgs, this.connectionString, this.root.isEmulator, ext.outputChannel, timeout);
}
private async _determineShellPathOrCmd(shellPathSetting: string): Promise<void> {
@ -201,15 +215,6 @@ export class MongoDatabaseTreeItem extends AzureParentTreeItem<IMongoTreeRoot> {
}
}
}
private async createShell(shellPath: string): Promise<Shell> {
return <Promise<null>>Shell.create(shellPath, this.connectionString, this.root.isEmulator)
.then(
shell => {
return shell.useDatabase(this.databaseName).then(() => shell);
},
error => vscode.window.showErrorMessage(error));
}
}
export function validateMongoCollectionName(collectionName: string): string | undefined | null {

Просмотреть файл

@ -0,0 +1,170 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as cp from 'child_process';
import * as os from 'os';
import { isNumber } from 'util';
import * as vscode from 'vscode';
import { Event, EventEmitter } from 'vscode';
import { parseError } from 'vscode-azureextensionui';
import { improveError } from './improveError';
// We add these when we display to the output window
const stdInPrefix = '> ';
const stdErrPrefix = 'ERR> ';
const errorPrefix = 'Error: ';
const processStartupTimeout = 60;
export interface IInteractiveChildProcessOptions {
command: string;
args: string[];
outputChannel?: vscode.OutputChannel;
workingDirectory?: string;
showTimeInOutputChannel?: boolean;
outputFilterSearch?: RegExp;
outputFilterReplace?: string;
}
export class InteractiveChildProcess {
private _childProc: cp.ChildProcess;
private readonly _options: IInteractiveChildProcessOptions;
private _startTime: number;
private _error: unknown;
private _isKilling: boolean;
private constructor(options: IInteractiveChildProcessOptions) {
this._options = options;
}
private readonly _onStdOutEmitter: EventEmitter<string> = new EventEmitter<string>();
public readonly onStdOut: Event<string> = this._onStdOutEmitter.event;
private readonly _onStdErrEmitter: EventEmitter<string> = new EventEmitter<string>();
public readonly onStdErr: Event<string> = this._onStdErrEmitter.event;
private readonly _onErrorEmitter: EventEmitter<unknown> = new EventEmitter<unknown>();
public readonly onError: Event<unknown> = this._onErrorEmitter.event;
public static async create(options: IInteractiveChildProcessOptions): Promise<InteractiveChildProcess> {
let child: InteractiveChildProcess = new InteractiveChildProcess(options);
await child.startCore();
return child;
}
public kill(): void {
this._isKilling = true;
this._childProc.kill();
}
public writeLine(text: string): void {
this.writeLineToOutputChannel(text, stdInPrefix);
this._childProc.stdin.write(text + os.EOL);
}
private async startCore(): Promise<void> {
this._startTime = Date.now();
const formattedArgs: string = this._options.args.join(' ');
let workingDirectory = this._options.workingDirectory || os.tmpdir();
const options: cp.SpawnOptions = {
cwd: workingDirectory,
// Using shell=true would mean that we can pass paths that will be resolved by the shell, but since
// the command is run in the shell, handling errors (such as command not found) would be more indirect,
// coming through STDERR instead of the error event
shell: false
};
this.writeLineToOutputChannel(`Starting executable: "${this._options.command}" ${formattedArgs}`);
this._childProc = cp.spawn(this._options.command, this._options.args, options);
this._childProc.stdout.on('data', (data: string | Buffer) => {
let text = data.toString();
this._onStdOutEmitter.fire(text);
this.writeLineToOutputChannel(text);
});
this._childProc.stderr.on('data', (data: string | Buffer) => {
let text = data.toString();
this._onStdErrEmitter.fire(text);
this.writeLineToOutputChannel(text, stdErrPrefix);
});
this._childProc.on('error', (error: unknown) => {
let improvedError = improveError(error);
this.setError(improvedError);
});
this._childProc.on('close', (code: number | null) => {
if (isNumber(code) && code !== 0) {
this.setError(`The process exited with code ${code}.`);
} else if (!this._isKilling) {
this.setError(`The process exited prematurely.`);
}
});
// Wait for the process to start up
await new Promise<void>(async (resolve, reject) => {
const started = Date.now();
// tslint:disable-next-line:promise-must-complete no-constant-condition
while (true) {
if (!!this._error || this._isKilling) {
reject(this._error);
break;
} else if (!!this._childProc.pid) {
resolve();
break;
} else {
if (Date.now() > started + processStartupTimeout) {
reject("The process did not start in a timely manner");
break;
}
await delay(50);
}
}
});
}
private writeLineToOutputChannel(text: string, displayPrefix?: string): void {
let filteredText = this.filterText(text);
let changedIntoEmptyString = (filteredText !== text && filteredText === '');
if (!changedIntoEmptyString) {
text = filteredText;
if (this._options.outputChannel) {
if (this._options.showTimeInOutputChannel) {
let ms = Date.now() - this._startTime;
text = `${ms}ms: ${text}`;
}
text = (displayPrefix || "") + text;
this._options.outputChannel.appendLine(text);
}
}
}
private setError(error: unknown): void {
this.writeLineToOutputChannel(parseError(error).message, errorPrefix);
this._error = this._error || error;
this._onErrorEmitter.fire(error);
}
private filterText(text: string): string {
if (this._options.outputFilterSearch) {
let filtered = text.replace(this._options.outputFilterSearch, this._options.outputFilterReplace || "");
return filtered;
}
return text;
}
}
async function delay(milliseconds: number): Promise<void> {
return new Promise(resolve => {
// tslint:disable-next-line:no-string-based-set-timeout // false positive
setTimeout(resolve, milliseconds);
});
}

17
src/utils/improveError.ts Normal file
Просмотреть файл

@ -0,0 +1,17 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { parseError } from "vscode-azureextensionui";
export function improveError(error: unknown): unknown {
let message = parseError(error).message;
// Example: "spawn c:\Program Files\MongoDB\Server\4.0\bin\mongo.exe ENOENT"
let match = message.match(/spawn (.*) ENOENT/);
if (match) {
return new Error(`Could not find ${match[1]}`);
}
return error;
}

24
src/utils/wrapError.ts Normal file
Просмотреть файл

@ -0,0 +1,24 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as os from 'os';
import { parseError } from "vscode-azureextensionui";
export function wrapError(outerError?: unknown, innerError?: unknown): unknown {
if (!innerError) {
return outerError;
} else if (!outerError) {
return innerError;
}
let innerMessage = parseError(innerError).message;
let outerMessage = parseError(outerError).message;
if (outerError instanceof Error) {
outerError.message = `${outerError.message}${os.EOL}${innerMessage}`;
return outerError;
}
return new Error(`${outerMessage}${os.EOL}${innerMessage}`);
}

24
test/improveError.test.ts Normal file
Просмотреть файл

@ -0,0 +1,24 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { improveError } from '../extension.bundle';
import { parseError } from 'vscode-azureextensionui';
suite("improveError", () => {
test("no change", () => {
let msg: string = "where is c:\\Program Files\\MongoDB\Server\\4.0\\bin\\mongo.exe?";
let improved: unknown = improveError(msg);
assert.equal(parseError(improved).message, msg);
});
test("spawn ENOENT", () => {
let msg: string = "spawn c:\\Program Files\\MongoDB\Server\\4.0\\bin\\mongo.exe ENOENT";
let improved: unknown = improveError(msg);
assert.equal(parseError(improved).message, "Could not find c:\\Program Files\\MongoDB\Server\\4.0\\bin\\mongo.exe");
});
});

281
test/mongoShell.test.ts Normal file
Просмотреть файл

@ -0,0 +1,281 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// CONSIDER: Run in pipeline
import * as assert from 'assert';
import { parseError, MongoShell, isWindows } from '../extension.bundle';
import * as cp from "child_process";
import { isNumber } from 'util';
import * as os from 'os';
import * as path from 'path';
import { setEnvironmentVariables } from './util/setEnvironmentVariables';
import { IDisposable } from '../src/utils/vscodeUtils';
import * as fse from 'fs-extra';
import * as vscode from 'vscode';
const VERBOSE = false; // If true, the output from the Mongo server and shell will be sent to the console for debugging purposes
let testsSupported: boolean = true;
if (!isWindows) {
// CONSIDER: Non-Windows
console.warn(`Not running in Windows - skipping MongoShell tests`);
testsSupported = false;
}
suite("MongoShell", function (this: Mocha.Suite) {
function testIfSupported(title: string, fn?: Mocha.Func | Mocha.AsyncFunc): void {
if (testsSupported) {
test(title, fn);
} else {
test(title);
}
}
// CONSIDER: Make more generic
let mongodCP: cp.ChildProcess;
let mongodPath = "c:\\Program Files\\MongoDB\\Server\\4.2\\bin\\mongod.exe";
let mongoPath = "c:\\Program Files\\MongoDB\\Server\\4.2\\bin\\mongo.exe";
let mongoDOutput = "";
let mongoDErrors = "";
let isClosed = false;
if (!fse.existsSync(mongodPath)) {
console.log(`Couldn't find mongod.exe at ${mongodPath} - skipping MongoShell tests`);
testsSupported = false;
} else if (!fse.existsSync(mongodPath)) {
console.log(`Couldn't find mongo.exe at ${mongoPath} - skipping MongoShell tests`);
testsSupported = false;
}
class FakeOutputChannel implements vscode.OutputChannel {
public name: string;
public output: string;
append(value: string): void {
assert(value !== undefined);
assert(!value.includes('undefined'));
this.output = this.output ? this.output + os.EOL + value : value;
log(value, "Output channel: ");
}
appendLine(value: string): void {
assert(value !== undefined);
this.append(value + os.EOL);
}
clear(): void { }
show(preserveFocus?: boolean): void;
show(column?: vscode.ViewColumn, preserveFocus?: boolean): void;
show(_column?: any, _preserveFocus?: any) { }
hide(): void { }
dispose(): void { }
}
function log(text: string, linePrefix: string): void {
text = text.replace(/(^|[\r\n]+)/g, "$1" + linePrefix)
if (VERBOSE) {
console.log(text);
}
}
async function delay(milliseconds: number): Promise<void> {
return new Promise(resolve => {
setTimeout(resolve, milliseconds);
})
}
function executeInShell(command: string): string {
return cp.execSync(command,
{
}).toString();
}
suiteSetup(() => {
if (testsSupported) {
assert(fse.existsSync(mongodPath), "Couldn't find mongod.exe at " + mongodPath);
assert(fse.existsSync(mongoPath), "Couldn't find mongo.exe at " + mongoPath);
// Shut down any still-running mongo server
try {
executeInShell('taskkill /f /im mongod.exe');
} catch (error) {
assert(/The process .* not found/.test(parseError(error).message), `Error killing mongod: ${parseError(error).message}`);
}
mongodCP = cp.spawn(mongodPath, ['--quiet']);
mongodCP.stdout.on("data", (buffer: Buffer) => {
log(buffer.toString(), "mongo server: ");
mongoDOutput += buffer.toString();
});
mongodCP.stderr.on("data", (buffer: Buffer) => {
log(buffer.toString(), "mongo server STDERR: ");
mongoDErrors += buffer.toString();
});
mongodCP.on("error", (error: unknown) => {
log(parseError(error).message, "mongo server Error: ");
mongoDErrors += parseError(error).message + os.EOL;
});
mongodCP.on("close", (code?: number) => {
console.log("mongo server: Close code=" + code);
isClosed = true;
if (isNumber(code) && code !== 0) {
mongoDErrors += "mongo server: Closed with code " + code + os.EOL;
}
});
}
});
suiteTeardown(() => {
if (mongodCP) {
mongodCP.kill();
}
});
testIfSupported("Verify mongod running", async () => {
while (!mongoDOutput.includes('waiting for connections on port 27017')) {
assert.equal(mongoDErrors, "", "Expected no errors");
assert(!isClosed);
await delay(50);
}
});
function testShellCommand(options: {
script: string;
expectedResult?: string;
expectedError?: string | RegExp;
expectedOutput?: RegExp;
title?: string; // Defaults to script
args?: string[]; // Defaults to []
mongoPath?: string; // Defaults to the correct mongo path
env?: { [key: string]: string }; // Add to environment
timeoutSeconds?: number;
}): void {
testIfSupported(options.title || options.script, async () => {
assert(!isClosed);
assert(mongoDErrors === "");
let previousEnv: IDisposable;
let shell: MongoShell | undefined;
let outputChannel = new FakeOutputChannel();
try {
previousEnv = setEnvironmentVariables(options.env || {});
shell = await MongoShell.create(options.mongoPath || mongoPath, options.args || [], '', false, outputChannel, options.timeoutSeconds || 5);
let result = await shell.executeScript(options.script);
if (options.expectedError) {
assert(false, `Expected error did not occur: '${options.expectedError}'`);
}
if (options.expectedResult !== undefined) {
assert.equal(result, options.expectedResult);
}
} catch (error) {
let message = parseError(error).message;
if (options.expectedError instanceof RegExp) {
assert(options.expectedError.test(message), `Actual error did not match expected error regex. Actual error: ${message}`)
} else if (typeof options.expectedError === 'string') {
assert.equal(message, options.expectedError);
} else {
assert(false, `Unexpected error during the test: ${message}`);
}
if (options.expectedOutput instanceof RegExp) {
assert(options.expectedOutput.test(outputChannel.output), `Actual contents written to output channel did not match expected regex. Actual output channel contents: ${outputChannel.output}`)
}
} finally {
if (shell) {
shell.dispose();
}
if (previousEnv) {
previousEnv.dispose();
}
}
});
}
testShellCommand({
script: 'use abc',
expectedResult: 'switched to db abc'
});
testShellCommand({
title: "Incorrect path",
script: 'use abc',
mongoPath: "/notfound/mongo.exe",
expectedError: /Could not find .*notfound.*mongo.exe/
});
testShellCommand({
title: "Find mongo through PATH",
script: 'use abc',
mongoPath: "mongo",
expectedResult: 'switched to db abc',
env: {
PATH: process.env["path"] + ";" + path.dirname(mongoPath)
}
});
testShellCommand({
title: "With valid argument",
script: 'use abc',
args: ["--quiet"],
expectedResult: 'switched to db abc'
});
testShellCommand({
title: "With invalid argument",
script: '',
args: ["--hey-man-how-are-you"],
expectedError: /Error parsing command line: unrecognised option/
});
testShellCommand({
title: "Output window may contain additional information",
script: '',
args: ["-u", "baduser", "-p", "badpassword"],
expectedError: /The output window may contain additional information/
});
testShellCommand({
title: "With bad credentials",
script: '',
args: ["-u", "baduser", "-p", "badpassword"],
expectedError: /The process exited with code 1/,
expectedOutput: /Authentication failed/
});
testShellCommand({
title: "Process exits immediately",
script: '',
args: ["--version"],
expectedError: /The process exited prematurely/
});
testShellCommand({
title: "Javascript",
script: "for (var i = 0; i < 123; ++i) { }; i",
expectedResult: "123"
});
testShellCommand({
title: "Actual timeout",
script: "for (var i = 0; i < 10000000; ++i) { }; i",
expectedError: /Timed out trying to execute the Mongo script. To use a longer timeout, modify the VS Code 'mongo.shell.timeout' setting./,
timeoutSeconds: 2
});
testIfSupported("More results than displayed (type 'it' for more -> (More))", async () => {
let shell = await MongoShell.create(mongoPath, [], '', false, new FakeOutputChannel(), 5);
await shell.executeScript('db.mongoShellTest.drop()');
await shell.executeScript('for (var i = 0; i < 50; ++i) { db.mongoShellTest.insert({a:i}); }');
let result = await shell.executeScript('db.mongoShellTest.find().pretty()');
assert(!result.includes('Type "it" for more'));
assert(result.includes('(More)'));
shell.dispose();
});
});

Просмотреть файл

@ -0,0 +1,36 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { setEnvironmentVariables } from './setEnvironmentVariables';
import { isWindows } from '../../extension.bundle';
suite("setEnvironmentVariables (test util)", () => {
test("restore", () => {
let currentPath = process.env.PATH;
let dispose = setEnvironmentVariables({ PATH: "new path" });
assert.equal(process.env.PATH, 'new path');
dispose.dispose();
assert.equal(process.env.PATH, currentPath);
});
test("different casings (Windows)", () => {
if (isWindows) {
let currentPath = process.env["paTH"];
let dispose = setEnvironmentVariables({ "PAth": "new path" });
assert.equal(process.env["path"], 'new path');
assert.equal(process.env["PATH"], 'new path');
dispose.dispose();
assert.equal(process.env["path"], currentPath);
assert.equal(process.env["PATH"], currentPath);
}
});
});

Просмотреть файл

@ -0,0 +1,36 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IDisposable } from "../../extension.bundle";
/**
* Add a set of environment variables, and return to the previous values after disposing the result
*/
export function setEnvironmentVariables(env: { [key: string]: string }): IDisposable {
let setRestoreEnv = new SetRestoreEnv();
setRestoreEnv.set(env);
return setRestoreEnv;
}
class SetRestoreEnv implements IDisposable {
private _previousValues: { [key: string]: string } = {};
public set(env: { [key: string]: string }): void {
for (let key of Object.keys(env || {})) {
[this._previousValues[key], process.env[key]] = [process.env[key], env[key]];
}
}
public restore(): void {
for (let key of Object.keys(this._previousValues)) {
process.env[key] = this._previousValues[key];
}
}
public dispose(): void {
this.restore();
}
}

53
test/wrapError.test.ts Normal file
Просмотреть файл

@ -0,0 +1,53 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import * as os from 'os';
import { wrapError } from '../extension.bundle';
import { parseError } from 'vscode-azureextensionui';
suite("wrapError", () => {
test("just outer string", () => {
let wrapped = wrapError('Outer error')
assert(typeof wrapped === 'string');
assert.equal(wrapped, 'Outer error');
});
test("just outer error", () => {
let wrapped = wrapError(new Error('Outer error'));
assert(wrapped instanceof Error);
assert.equal(parseError(wrapped).message, 'Outer error');
});
test("just inner", () => {
let wrapped = wrapError(undefined, 'Inner error')
assert(typeof wrapped === 'string');
assert.equal(wrapped, 'Inner error');
});
test("outer string, inner string", () => {
let wrapped = wrapError('Outer error.', 'Inner error.')
assert(wrapped instanceof Error);
assert.equal(parseError(wrapped).message, `Outer error.${os.EOL}Inner error.`);
});
test("outer error, inner string", () => {
let wrapped = wrapError(new Error('Outer error.'), 'Inner error.')
assert(wrapped instanceof Error);
assert.equal(parseError(wrapped).message, `Outer error.${os.EOL}Inner error.`);
});
test("outer error, inner error", () => {
let wrapped = wrapError(new Error('Outer error.'), new Error('Inner error.'));
assert(wrapped instanceof Error);
assert.equal(parseError(wrapped).message, `Outer error.${os.EOL}Inner error.`);
});
test("outer string, inner error", () => {
let wrapped = wrapError('Outer error.', new Error('Inner error.'));
assert(wrapped instanceof Error);
assert(parseError(wrapped).message, `Outer error.${os.EOL}Inner error.`);
});
});