diff --git a/build/lib/extensions.js b/build/lib/extensions.js index 188d878a40..d48254fb2c 100644 --- a/build/lib/extensions.js +++ b/build/lib/extensions.js @@ -204,7 +204,8 @@ const externalExtensions = [ 'cms', 'query-history', 'liveshare', - 'sql-database-projects' + 'sql-database-projects', + 'machine-learning-services' ]; // extensions that require a rebuild since they have native parts const rebuildExtensions = [ diff --git a/build/lib/extensions.ts b/build/lib/extensions.ts index 8b9f92335c..33a25ad92d 100644 --- a/build/lib/extensions.ts +++ b/build/lib/extensions.ts @@ -240,7 +240,8 @@ const externalExtensions = [ 'cms', 'query-history', 'liveshare', - 'sql-database-projects' + 'sql-database-projects', + 'machine-learning-services' ]; // extensions that require a rebuild since they have native parts diff --git a/extensions/machine-learning-services/.gitignore b/extensions/machine-learning-services/.gitignore new file mode 100644 index 0000000000..155dd9d6e6 --- /dev/null +++ b/extensions/machine-learning-services/.gitignore @@ -0,0 +1,2 @@ +*.vsix +python/** diff --git a/extensions/machine-learning-services/.vscodeignore b/extensions/machine-learning-services/.vscodeignore new file mode 100644 index 0000000000..0cc3de2236 --- /dev/null +++ b/extensions/machine-learning-services/.vscodeignore @@ -0,0 +1,4 @@ +src/** +tsconfig.json +python/** +out/test/** diff --git a/extensions/machine-learning-services/README.md b/extensions/machine-learning-services/README.md new file mode 100644 index 0000000000..589184f0f1 --- /dev/null +++ b/extensions/machine-learning-services/README.md @@ -0,0 +1,28 @@ +# Machine Learning Services for Azure Data Studio # +Machine Learning Services for Azure Data Studio (Preview) provides support for new features that help you create, build, and deploy machine learning jobs in SQL Server through Azure Data Studio. + +## Features ## +* Enable Machine Learning Services on SQL Server. +* Deploy an MLFlow container to track models. +* Follow along with machine learning notebooks. + +## Prerequisites ## +In order to use Machine Learning Services for Azure Data Studio (Preview), your SQL Server must have Machine Learning Services installed. Follow the instructions [here](https://docs.microsoft.com/sql/advanced-analytics/install/sql-machine-learning-services-windows-install?view=sql-server-ver15) to do so. + +## Code of Conduct + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. + +## Privacy Statement + +The [Microsoft Enterprise and Developer Privacy Statement](https://privacy.microsoft.com/privacystatement) describes the privacy statement of this software. + +## License + +Copyright (c) Microsoft Corporation. All rights reserved. + +Licensed under the [Source EULA](https://raw.githubusercontent.com/Microsoft/azuredatastudio/master/LICENSE.txt). + + + + diff --git a/extensions/machine-learning-services/config.json b/extensions/machine-learning-services/config.json new file mode 100644 index 0000000000..cac2f5d38f --- /dev/null +++ b/extensions/machine-learning-services/config.json @@ -0,0 +1,6 @@ +{ + "requiredPythonPackages": [ + { "name": "pymssql", "version": "2.1.4" }, + { "name": "sqlmlutils", "version": ""} + ] +} diff --git a/extensions/machine-learning-services/images/ML_ExtensionIcon.png b/extensions/machine-learning-services/images/ML_ExtensionIcon.png new file mode 100644 index 0000000000..7bbcbce2c7 Binary files /dev/null and b/extensions/machine-learning-services/images/ML_ExtensionIcon.png differ diff --git a/extensions/machine-learning-services/package.json b/extensions/machine-learning-services/package.json new file mode 100644 index 0000000000..00a7e9acdd --- /dev/null +++ b/extensions/machine-learning-services/package.json @@ -0,0 +1,54 @@ +{ + "name": "machineLearningServices", + "displayName": "%displayName%", + "description": "%description%", + "version": "1.0.0", + "publisher": "Microsoft", + "preview": true, + "engines": { + "vscode": "^1.25.0", + "azdata": ">=1.13.0" + }, + "activationEvents": [ + "onCommand:ml.command.managePackages" + ], + "license": "https://raw.githubusercontent.com/Microsoft/azuredatastudio/master/LICENSE.txt", + "icon": "images/ML_ExtensionIcon.png", + "aiKey": "AIF-37eefaf0-8022-4671-a3fb-64752724682e", + "main": "./out/main", + "repository": { + "type": "git", + "url": "https://github.com/Microsoft/azuredatastudio.git" + }, + "extensionDependencies": [ + "Microsoft.mssql", + "Microsoft.notebook" + ], + "contributes": { + "commands": [ + { + "command": "ml.command.managePackages", + "title": "%ml.command.managePackages%" + } + ] + }, + "dependencies": { + "vscode-nls": "^4.0.0" + }, + "devDependencies": { + "@types/mocha": "^5.2.5", + "@types/node": "^10.14.8", + "@types/uuid": "^3.4.5", + "mocha": "^5.2.0", + "mocha-junit-reporter": "^1.17.0", + "mocha-multi-reporters": "^1.1.7", + "should": "^13.2.1", + "typemoq": "^2.1.0", + "vscode": "1.1.26" + }, + "__metadata": { + "id": "56", + "publisherDisplayName": "Microsoft", + "publisherId": "Microsoft" + } +} diff --git a/extensions/machine-learning-services/package.nls.json b/extensions/machine-learning-services/package.nls.json new file mode 100644 index 0000000000..2e3ddd2788 --- /dev/null +++ b/extensions/machine-learning-services/package.nls.json @@ -0,0 +1,10 @@ +{ + "displayName": "SQL Server Machine Learning Services", + "description": "SQL Server Machine Learning Services", + "mlServices.enable": "Enable Machine Learning Services", + "mlServices.disable": "Disable Machine Learning Services", + "title.tasks": "Getting Started", + "title.endpoints": "Endpoints", + "title.books": "Machine Learning Services Books", + "ml.command.managePackages": "Manage Packages in SQL Server" +} diff --git a/extensions/machine-learning-services/src/common/apiWrapper.ts b/extensions/machine-learning-services/src/common/apiWrapper.ts new file mode 100644 index 0000000000..6f390e3326 --- /dev/null +++ b/extensions/machine-learning-services/src/common/apiWrapper.ts @@ -0,0 +1,65 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import * as azdata from 'azdata'; + +/** + * Wrapper class to act as a facade over VSCode and Data APIs and allow us to test / mock callbacks into + * this API from our code + */ +export class ApiWrapper { + public createOutputChannel(name: string): vscode.OutputChannel { + return vscode.window.createOutputChannel(name); + } + + public createTerminalWithOptions(options: vscode.TerminalOptions): vscode.Terminal { + return vscode.window.createTerminal(options); + } + + public getCurrentConnection(): Thenable { + return azdata.connection.getCurrentConnection(); + } + + public getCredentials(connectionId: string): Thenable<{ [name: string]: string }> { + return azdata.connection.getCredentials(connectionId); + } + + public registerCommand(command: string, callback: (...args: any[]) => any, thisArg?: any): vscode.Disposable { + return vscode.commands.registerCommand(command, callback, thisArg); + } + + public executeCommand(command: string, ...rest: any[]): Thenable { + return vscode.commands.executeCommand(command, ...rest); + } + + public getUriForConnection(connectionId: string): Thenable { + return azdata.connection.getUriForConnection(connectionId); + } + + public getProvider(providerId: string, providerType: azdata.DataProviderType): T { + return azdata.dataprotocol.getProvider(providerId, providerType); + } + + public showErrorMessage(message: string, ...items: string[]): Thenable { + return vscode.window.showErrorMessage(message, ...items); + } + + public showInfoMessage(message: string, ...items: string[]): Thenable { + return vscode.window.showInformationMessage(message, ...items); + } + + public showOpenDialog(options: vscode.OpenDialogOptions): Thenable { + return vscode.window.showOpenDialog(options); + } + + public startBackgroundOperation(operationInfo: azdata.BackgroundOperationInfo): void { + azdata.tasks.startBackgroundOperation(operationInfo); + } + + public getExtension(extensionId: string): vscode.Extension | undefined { + return vscode.extensions.getExtension(extensionId); + } +} diff --git a/extensions/machine-learning-services/src/common/config.ts b/extensions/machine-learning-services/src/common/config.ts new file mode 100644 index 0000000000..e2fd1d8a3b --- /dev/null +++ b/extensions/machine-learning-services/src/common/config.ts @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { promises as fs } from 'fs'; +import * as path from 'path'; +import * as nbExtensionApis from '../typings/notebookServices'; + +const configFileName = 'config.json'; + +/** + * Extension Configuration + */ +export class Config { + + private _configValues: any; + + constructor(private _root: string) { + } + + /** + * Loads the config values + */ + public async load(): Promise { + const rawConfig = await fs.readFile(path.join(this._root, configFileName)); + this._configValues = JSON.parse(rawConfig.toString()); + } + + /** + * Returns the config value of required packages + */ + public get requiredPythonPackages(): nbExtensionApis.IPackageDetails[] { + return this._configValues.requiredPythonPackages; + } +} diff --git a/extensions/machine-learning-services/src/common/constants.ts b/extensions/machine-learning-services/src/common/constants.ts new file mode 100644 index 0000000000..d156cddb75 --- /dev/null +++ b/extensions/machine-learning-services/src/common/constants.ts @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as nls from 'vscode-nls'; + +const localize = nls.loadMessageBundle(); + +export const winPlatform = 'win32'; +export const pythonBundleVersion = '0.0.1'; +export const managePackagesCommand = 'jupyter.cmd.managePackages'; +export const mlManagePackagesCommand = 'ml.command.managePackages'; +export const extensionOutputChannel = 'Machine Learning Services'; +export const notebookExtensionName = 'Microsoft.notebook'; + +// Localized texts +// +export const managePackageCommandError = localize('ml.managePackages.error', "Either no connection is available or the server does not have external script enabled."); +export function installDependenciesError(err: string): string { return localize('ml.installDependencies.error', "Failed to install dependencies. Error: {0}", err); } +export const installDependenciesMsgTaskName = localize('ml.installDependencies.msgTaskName', "Installing Machine Learning extension dependencies"); +export const installDependenciesPackages = localize('ml.installDependencies.packages', "Installing required packages ..."); +export const installDependenciesPackagesAlreadyInstalled = localize('ml.installDependencies.packagesAlreadyInstalled', "Required packages are already installed."); +export function installDependenciesGetPackagesError(err: string): string { return localize('ml.installDependencies.getPackagesError', "Failed to get installed python packages. Error: {0}", err); } +export const packageManagerNoConnection = localize('ml.packageManager.NoConnection', "No connection selected"); +export const notebookExtensionNotLoaded = localize('ml.notebookExtensionNotLoaded', "Notebook extension is not loaded"); diff --git a/extensions/machine-learning-services/src/common/processService.ts b/extensions/machine-learning-services/src/common/processService.ts new file mode 100644 index 0000000000..d17b14d6ba --- /dev/null +++ b/extensions/machine-learning-services/src/common/processService.ts @@ -0,0 +1,81 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as vscode from 'vscode'; +import * as childProcess from 'child_process'; + +const ExecScriptsTimeoutInSeconds = 600000; +export class ProcessService { + + public async execScripts(exeFilePath: string, scripts: string[], outputChannel?: vscode.OutputChannel): Promise { + return new Promise((resolve, reject) => { + + const scriptExecution = childProcess.spawn(exeFilePath); + let output: string; + scripts.forEach(script => { + scriptExecution.stdin.write(`${script}\n`); + }); + scriptExecution.stdin.end(); + + // Add listeners to print stdout and stderr if an output channel was provided + if (outputChannel) { + scriptExecution.stdout.on('data', data => { + this.outputDataChunk(data, outputChannel, ' stdout: '); + output = output + data.toString(); + }); + scriptExecution.stderr.on('data', data => { + this.outputDataChunk(data, outputChannel, ' stderr: '); + output = output + data.toString(); + }); + } + + scriptExecution.on('exit', (code) => { + if (code === 0) { + resolve(); + } else { + reject(`Process exited with code: ${code}. output: ${output}`); + } + }); + setTimeout(() => { + try { + scriptExecution.kill(); + } catch (error) { + console.log(error); + } + }, ExecScriptsTimeoutInSeconds); + }); + } + + public async executeBufferedCommand(cmd: string, outputChannel?: vscode.OutputChannel): Promise { + return new Promise((resolve, reject) => { + if (outputChannel) { + outputChannel.appendLine(` > ${cmd}`); + } + + let child = childProcess.exec(cmd, (err, stdout) => { + if (err) { + reject(err); + } else { + resolve(stdout); + } + }); + + // Add listeners to print stdout and stderr if an output channel was provided + if (outputChannel) { + child.stdout.on('data', data => { this.outputDataChunk(data, outputChannel, ' stdout: '); }); + child.stderr.on('data', data => { this.outputDataChunk(data, outputChannel, ' stderr: '); }); + } + }); + } + + private outputDataChunk(data: string | Buffer, outputChannel: vscode.OutputChannel, header: string): void { + data.toString().split(/\r?\n/) + .forEach(line => { + outputChannel.appendLine(header + line); + }); + } +} diff --git a/extensions/machine-learning-services/src/common/queryRunner.ts b/extensions/machine-learning-services/src/common/queryRunner.ts new file mode 100644 index 0000000000..398c2342aa --- /dev/null +++ b/extensions/machine-learning-services/src/common/queryRunner.ts @@ -0,0 +1,124 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as azdata from 'azdata'; +import * as nbExtensionApis from '../typings/notebookServices'; +import { ApiWrapper } from './apiWrapper'; + +const listPythonPackagesQuery = ` +EXEC sp_execute_external_script +@language=N'Python', +@script=N'import pkg_resources +import pandas +OutputDataSet = pandas.DataFrame([(d.project_name, d.version) for d in pkg_resources.working_set])' +`; + +const checkMlInstalledQuery = ` +Declare @tablevar table(name NVARCHAR(MAX), min INT, max INT, config_value bit, run_value bit) +insert into @tablevar(name, min, max, config_value, run_value) exec sp_configure + +Declare @external_script_enabled bit +SELECT @external_script_enabled=config_value FROM @tablevar WHERE name = 'external scripts enabled' +SELECT @external_script_enabled`; + +const checkPythonInstalledQuery = ` + +SELECT is_installed +FROM sys.dm_db_external_language_stats s, sys.external_languages l +WHERE s.external_language_id = l.external_language_id AND language = 'Python'`; + +const modifyExternalScriptConfigQuery = ` + +EXEC sp_configure 'external scripts enabled', #CONFIG_VALUE#; +RECONFIGURE WITH OVERRIDE; + +Declare @tablevar table(name NVARCHAR(MAX), min INT, max INT, config_value bit, run_value bit) +insert into @tablevar(name, min, max, config_value, run_value) exec sp_configure + +Declare @external_script_enabled bit +SELECT @external_script_enabled=config_value FROM @tablevar WHERE name = 'external scripts enabled' +SELECT @external_script_enabled`; + +/** + * SQL Query runner + */ +export class QueryRunner { + + constructor(private _apiWrapper: ApiWrapper) { + } + + /** + * Returns python packages installed in SQL server instance + * @param connection SQL Connection + */ + public async getPythonPackages(connection: azdata.connection.ConnectionProfile): Promise { + let packages: nbExtensionApis.IPackageDetails[] = []; + let result = await this.runQuery(connection, listPythonPackagesQuery); + if (result && result.rows.length > 0) { + packages = result.rows.map(row => { + return { + name: row[0].displayValue, + version: row[1].displayValue + }; + }); + } + return packages; + } + + /** + * Updates External Script Config in a SQL server instance + * @param connection SQL Connection + * @param enable if true the config will be enabled otherwise it will be disabled + */ + public async updateExternalScriptConfig(connection: azdata.connection.ConnectionProfile, enable: boolean): Promise { + let query = modifyExternalScriptConfigQuery; + let configValue = enable ? '1' : '0'; + query = query.replace('#CONFIG_VALUE#', configValue); + + await this.runQuery(connection, query); + } + + /** + * Returns true if python installed in the give SQL server instance + */ + public async isPythonInstalled(connection: azdata.connection.ConnectionProfile): Promise { + let result = await this.runQuery(connection, checkPythonInstalledQuery); + let isInstalled = false; + if (result && result.rows && result.rows.length > 0) { + isInstalled = result.rows[0][0].displayValue === '1'; + } + return isInstalled; + } + + /** + * Returns true if mls is installed in the give SQL server instance + */ + public async isMachineLearningServiceEnabled(connection: azdata.connection.ConnectionProfile): Promise { + let result = await this.runQuery(connection, checkMlInstalledQuery); + let isEnabled = false; + if (result && result.rows && result.rows.length > 0) { + isEnabled = result.rows[0][0].displayValue === '1'; + } + return isEnabled; + } + + private async runQuery(connection: azdata.connection.ConnectionProfile, query: string): Promise { + let result: azdata.SimpleExecuteResult | undefined = undefined; + try { + if (connection) { + let connectionUri = await this._apiWrapper.getUriForConnection(connection.connectionId); + let queryProvider = this._apiWrapper.getProvider(connection.providerId, azdata.DataProviderType.QueryProvider); + if (queryProvider) { + result = await queryProvider.runQueryAndReturn(connectionUri, query); + } + } + } catch (error) { + console.log(error); + } + return result; + } +} diff --git a/extensions/machine-learning-services/src/common/utils.ts b/extensions/machine-learning-services/src/common/utils.ts new file mode 100644 index 0000000000..d23367a301 --- /dev/null +++ b/extensions/machine-learning-services/src/common/utils.ts @@ -0,0 +1,48 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as uuid from 'uuid'; +import * as path from 'path'; +import * as os from 'os'; +import * as fs from 'fs'; +import * as constants from '../common/constants'; +import { promisify } from 'util'; + +export async function execCommandOnTempFile(content: string, command: (filePath: string) => Promise): Promise { + let tempFilePath: string = ''; + try { + tempFilePath = path.join(os.tmpdir(), `ads_ml_temp_${uuid.v4()}`); + await fs.promises.writeFile(tempFilePath, content); + let result = await command(tempFilePath); + return result; + } + finally { + await fs.promises.unlink(tempFilePath); + } +} + +export async function exists(path: string): Promise { + return promisify(fs.exists)(path); +} + +export async function createFolder(dirPath: string): Promise { + let folderExists = await exists(dirPath); + if (!folderExists) { + await fs.promises.mkdir(dirPath); + } +} + +export function getPythonInstallationLocation(rootFolder: string) { + return path.join(rootFolder, 'python'); +} + +export function getPythonExePath(rootFolder: string): string { + return path.join( + getPythonInstallationLocation(rootFolder), + constants.pythonBundleVersion, + process.platform === constants.winPlatform ? 'python.exe' : 'bin/python3'); +} diff --git a/extensions/machine-learning-services/src/controllers/mainController.ts b/extensions/machine-learning-services/src/controllers/mainController.ts new file mode 100644 index 0000000000..c551f4e4db --- /dev/null +++ b/extensions/machine-learning-services/src/controllers/mainController.ts @@ -0,0 +1,112 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as vscode from 'vscode'; +import * as nbExtensionApis from '../typings/notebookServices'; +import { PackageManager } from '../packageManagement/packageManager'; +import * as constants from '../common/constants'; +import { ApiWrapper } from '../common/apiWrapper'; +import { QueryRunner } from '../common/queryRunner'; +import { ProcessService } from '../common/processService'; +import { Config } from '../common/config'; + +/** + * The main controller class that initializes the extension + */ +export default class MainController implements vscode.Disposable { + + private _outputChannel: vscode.OutputChannel; + private _rootPath = this._context.extensionPath; + private _config: Config; + + public constructor( + private _context: vscode.ExtensionContext, + private _apiWrapper: ApiWrapper, + private _queryRunner: QueryRunner, + private _processService: ProcessService, + private _packageManager?: PackageManager + ) { + this._outputChannel = this._apiWrapper.createOutputChannel(constants.extensionOutputChannel); + this._rootPath = this._context.extensionPath; + this._config = new Config(this._rootPath); + } + + /** + * Deactivates the extension + */ + public deactivate(): void { + } + + /** + * Activates the extension + */ + public async activate(): Promise { + await this.initialize(); + return Promise.resolve(true); + } + + /** + * Returns an instance of Server Installation from notebook extension + */ + private async getNotebookExtensionApis(): Promise { + let nbExtension = this._apiWrapper.getExtension(constants.notebookExtensionName); + if (nbExtension) { + await nbExtension.activate(); + return (nbExtension.exports as nbExtensionApis.IExtensionApi); + } else { + throw new Error(constants.notebookExtensionNotLoaded); + } + } + + private async initialize(): Promise { + this._outputChannel.show(true); + let nbApis = await this.getNotebookExtensionApis(); + await this._config.load(); + + let packageManager = this.getPackageManager(nbApis); + this._apiWrapper.registerCommand(constants.mlManagePackagesCommand, (async () => { + await packageManager.managePackages(); + })); + try { + await packageManager.installDependencies(); + } catch (err) { + this._outputChannel.appendLine(err); + } + } + + /** + * Returns the package manager instance + */ + public getPackageManager(nbApis: nbExtensionApis.IExtensionApi): PackageManager { + if (!this._packageManager) { + this._packageManager = new PackageManager(nbApis, this._outputChannel, this._rootPath, this._apiWrapper, this._queryRunner, this._processService, this._config); + this._packageManager.init(); + } + return this._packageManager; + } + + /** + * Package manager instance + */ + public set packageManager(value: PackageManager) { + this._packageManager = value; + } + + /** + * Config instance + */ + public get config(): Config { + return this._config; + } + + /** + * Disposes the extension + */ + public dispose(): void { + this.deactivate(); + } +} diff --git a/extensions/machine-learning-services/src/main.ts b/extensions/machine-learning-services/src/main.ts new file mode 100644 index 0000000000..264d46081f --- /dev/null +++ b/extensions/machine-learning-services/src/main.ts @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; +import * as vscode from 'vscode'; +import MainController from './controllers/mainController'; +import { ApiWrapper } from './common/apiWrapper'; +import { QueryRunner } from './common/queryRunner'; +import { ProcessService } from './common/processService'; + +let controllers: MainController[] = []; + +export async function activate(context: vscode.ExtensionContext): Promise { + + let apiWrapper = new ApiWrapper(); + let queryRunner = new QueryRunner(apiWrapper); + let processService = new ProcessService(); + + // Start the main controller + // + let mainController = new MainController(context, apiWrapper, queryRunner, processService); + controllers.push(mainController); + context.subscriptions.push(mainController); + + await mainController.activate(); +} + +export function deactivate(): void { + for (let controller of controllers) { + controller.deactivate(); + } +} diff --git a/extensions/machine-learning-services/src/packageManagement/packageManager.ts b/extensions/machine-learning-services/src/packageManagement/packageManager.ts new file mode 100644 index 0000000000..6a5b51d840 --- /dev/null +++ b/extensions/machine-learning-services/src/packageManagement/packageManager.ts @@ -0,0 +1,156 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as vscode from 'vscode'; +import * as azdata from 'azdata'; +import * as nbExtensionApis from '../typings/notebookServices'; +import { SqlPythonPackageManageProvider } from './sqlPackageManageProvider'; +import { QueryRunner } from '../common/queryRunner'; +import * as utils from '../common/utils'; +import * as constants from '../common/constants'; +import { ApiWrapper } from '../common/apiWrapper'; +import { ProcessService } from '../common/processService'; +import { Config } from '../common/config'; +import { isNullOrUndefined } from 'util'; + +export class PackageManager { + + private _pythonExecutable: string = ''; + private _pythonInstallationLocation: string = ''; + private _sqlPackageManager: SqlPythonPackageManageProvider | undefined = undefined; + + /** + * Creates a new instance of PackageManager + */ + constructor( + private _nbExtensionApis: nbExtensionApis.IExtensionApi, + private _outputChannel: vscode.OutputChannel, + private _rootFolder: string, + private _apiWrapper: ApiWrapper, + private _queryRunner: QueryRunner, + private _processService: ProcessService, + private _config: Config) { + } + + /** + * Initializes the instance and resister SQL package manager with manage package dialog + */ + public init(): void { + this._pythonInstallationLocation = utils.getPythonInstallationLocation(this._rootFolder); + this._pythonExecutable = utils.getPythonExePath(this._rootFolder); + this._sqlPackageManager = new SqlPythonPackageManageProvider(this._nbExtensionApis, this._outputChannel, this._rootFolder, this._apiWrapper, this._queryRunner, this._processService); + this._nbExtensionApis.registerPackageManager(SqlPythonPackageManageProvider.ProviderId, this._sqlPackageManager); + } + + /** + * Executes manage package command for SQL server packages. + */ + public async managePackages(): Promise { + + // Only execute the command if there's a valid connection with ml configuration enabled + // + let connection = await this.getCurrentConnection(); + let isPythonInstalled = await this._queryRunner.isPythonInstalled(connection); + if (connection && isPythonInstalled && this._sqlPackageManager) { + this._apiWrapper.executeCommand(constants.managePackagesCommand, { + multiLocations: false, + defaultLocation: this._sqlPackageManager.packageTarget.location, + defaultProviderId: SqlPythonPackageManageProvider.ProviderId + }); + } else { + this._apiWrapper.showInfoMessage(constants.managePackageCommandError); + } + } + + /** + * Installs dependencies for the extension + */ + public async installDependencies(): Promise { + return new Promise((resolve, reject) => { + let msgTaskName = constants.installDependenciesMsgTaskName; + this._apiWrapper.startBackgroundOperation({ + displayName: msgTaskName, + description: msgTaskName, + isCancelable: false, + operation: async op => { + try { + if (!(await utils.exists(this._pythonExecutable))) { + // Install python + // + await utils.createFolder(this._pythonInstallationLocation); + await this.jupyterInstallation.installPythonPackage(op, false, this._pythonInstallationLocation, this._outputChannel); + } + + // Install required packages + // + await this.installRequiredPythonPackages(); + op.updateStatus(azdata.TaskStatus.Succeeded); + resolve(); + } catch (error) { + let errorMsg = constants.installDependenciesError(error ? error.message : ''); + op.updateStatus(azdata.TaskStatus.Failed, errorMsg); + reject(errorMsg); + } + } + }); + }); + } + + /** + * Installs required python packages + */ + private async installRequiredPythonPackages(): Promise { + let installedPackages = await this.getInstalledPipPackages(); + let fileContent = ''; + this._config.requiredPythonPackages.forEach(packageDetails => { + let hasVersion = ('version' in packageDetails) && !isNullOrUndefined(packageDetails['version']) && packageDetails['version'].length > 0; + if (!installedPackages.find(x => x.name === packageDetails['name'] && (!hasVersion || packageDetails['version'] === x.version))) { + let packageNameDetail = hasVersion ? `${packageDetails.name}==${packageDetails.version}` : `${packageDetails.name}`; + fileContent = `${fileContent}${packageNameDetail}\n`; + } + }); + + if (fileContent) { + this._outputChannel.appendLine(constants.installDependenciesPackages); + let result = await utils.execCommandOnTempFile(fileContent, async (tempFilePath) => { + return await this.installPackages(tempFilePath); + }); + this._outputChannel.appendLine(result); + } else { + this._outputChannel.appendLine(constants.installDependenciesPackagesAlreadyInstalled); + } + } + + private async getInstalledPipPackages(): Promise { + try { + let cmd = `"${this._pythonExecutable}" -m pip list --format=json`; + let packagesInfo = await this._processService.executeBufferedCommand(cmd, this._outputChannel); + let packagesResult: nbExtensionApis.IPackageDetails[] = []; + if (packagesInfo) { + packagesResult = JSON.parse(packagesInfo); + } + return packagesResult; + } + catch (err) { + this._outputChannel.appendLine(constants.installDependenciesGetPackagesError(err ? err.message : '')); + return []; + } + } + + private async getCurrentConnection(): Promise { + return await this._apiWrapper.getCurrentConnection(); + } + + private get jupyterInstallation(): nbExtensionApis.IJupyterServerInstallation { + return this._nbExtensionApis.getJupyterController().jupyterInstallation; + } + + private async installPackages(requirementFilePath: string): Promise { + let cmd = `"${this._pythonExecutable}" -m pip install -r "${requirementFilePath}"`; + return await this._processService.executeBufferedCommand(cmd, this._outputChannel); + } +} diff --git a/extensions/machine-learning-services/src/packageManagement/sqlPackageManageProvider.ts b/extensions/machine-learning-services/src/packageManagement/sqlPackageManageProvider.ts new file mode 100644 index 0000000000..a1677d5a3c --- /dev/null +++ b/extensions/machine-learning-services/src/packageManagement/sqlPackageManageProvider.ts @@ -0,0 +1,183 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as vscode from 'vscode'; +import * as azdata from 'azdata'; +import * as nbExtensionApis from '../typings/notebookServices'; +import * as utils from '../common/utils'; +import * as constants from '../common/constants'; +import { QueryRunner } from '../common/queryRunner'; +import { ApiWrapper } from '../common/apiWrapper'; +import { ProcessService } from '../common/processService'; + +const installMode = 'install'; +const uninstallMode = 'uninstall'; +const localPythonProviderId = 'localhost_Pip'; + +/** + * Manage Package Provider for python packages inside SQL server databases + */ +export class SqlPythonPackageManageProvider implements nbExtensionApis.IPackageManageProvider { + + private _pythonExecutable: string; + + public static ProviderId = 'sql_Python'; + + /** + * Creates new a instance + */ + constructor( + private _nbExtensionApis: nbExtensionApis.IExtensionApi, + private _outputChannel: vscode.OutputChannel, + private _rootFolder: string, + private _apiWrapper: ApiWrapper, + private _queryRunner: QueryRunner, + private _processService: ProcessService) { + this._pythonExecutable = utils.getPythonExePath(this._rootFolder); + } + + /** + * Returns provider Id + */ + public get providerId(): string { + return SqlPythonPackageManageProvider.ProviderId; + } + + /** + * Returns package target + */ + public get packageTarget(): nbExtensionApis.IPackageTarget { + return { location: 'SQL', packageType: 'Python' }; + } + + /** + * Returns list of packages + */ + public async listPackages(): Promise { + let packages = await this._queryRunner.getPythonPackages(await this.getCurrentConnection()); + if (packages) { + packages = packages.sort((a, b) => a.name.localeCompare(b.name)); + } else { + packages = []; + } + return packages; + } + + /** + * Installs given packages + * @param packages Packages to install + * @param useMinVersion minimum version + */ + async installPackages(packages: nbExtensionApis.IPackageDetails[], useMinVersion: boolean): Promise { + if (packages) { + + // TODO: install package as parallel + for (let index = 0; index < packages.length; index++) { + const element = packages[index]; + await this.updatePackage(element, installMode); + } + } + //TODO: use useMinVersion + console.log(useMinVersion); + } + + /** + * Execute a script to install or uninstall a python package inside current SQL Server connection + * @param packageDetails Packages to install or uninstall + * @param scriptMode can be 'install' or 'uninstall' + */ + private async updatePackage(packageDetails: nbExtensionApis.IPackageDetails, scriptMode: string): Promise { + let connection = await this.getCurrentConnection(); + let credentials = await this._apiWrapper.getCredentials(connection.connectionId); + + if (connection) { + let port = '1433'; + let server = connection.serverName; + let database = connection.databaseName ? `, database="${connection.databaseName}"` : ''; + let index = connection.serverName.indexOf(','); + if (index > 0) { + port = connection.serverName.substring(index + 1); + server = connection.serverName.substring(0, index); + } + + let pythonConnectionParts = `server="${server}", port=${port}, uid="${connection.userName}", pwd="${credentials[azdata.ConnectionOptionSpecialType.password]}"${database})`; + let pythonCommandScript = scriptMode === installMode ? + `pkgmanager.install(package="${packageDetails.name}", version="${packageDetails.version}")` : + `pkgmanager.uninstall(package_name="${packageDetails.name}")`; + + let scripts: string[] = [ + 'import sqlmlutils', + `connection = sqlmlutils.ConnectionInfo(driver="ODBC Driver 17 for SQL Server", ${pythonConnectionParts}`, + 'pkgmanager = sqlmlutils.SQLPackageManager(connection)', + pythonCommandScript + ]; + await this._processService.execScripts(this._pythonExecutable, scripts, this._outputChannel); + } + } + + /** + * Uninstalls given packages + * @param packages Packages to uninstall + */ + async uninstallPackages(packages: nbExtensionApis.IPackageDetails[]): Promise { + for (let index = 0; index < packages.length; index++) { + const element = packages[index]; + await this.updatePackage(element, uninstallMode); + } + } + + /** + * Returns true if the provider can be used + */ + async canUseProvider(): Promise { + let connection = await this.getCurrentConnection(); + if (connection && await this._queryRunner.isPythonInstalled(connection)) { + return true; + } + return false; + } + + /** + * Returns package overview for given name + * @param packageName Package Name + */ + async getPackageOverview(packageName: string): Promise { + let packagePreview: nbExtensionApis.IPackageOverview = { + name: packageName, + versions: [], + summary: '' + }; + let pythonPackageProvider = this.pythonPackageProvider; + if (pythonPackageProvider) { + packagePreview = await pythonPackageProvider.getPackageOverview(packageName); + } + return packagePreview; + } + + /** + * Returns location title + */ + async getLocationTitle(): Promise { + let connection = await this.getCurrentConnection(); + if (connection) { + return `${connection.serverName} ${connection.databaseName ? connection.databaseName : ''}`; + } + return constants.packageManagerNoConnection; + } + + private get pythonPackageProvider(): nbExtensionApis.IPackageManageProvider | undefined { + let providers = this._nbExtensionApis.getPackageManagers(); + if (providers && providers.has(localPythonProviderId)) { + return providers.get(localPythonProviderId); + } + return undefined; + } + + private async getCurrentConnection(): Promise { + return await this._apiWrapper.getCurrentConnection(); + } +} diff --git a/extensions/machine-learning-services/src/test/index.ts b/extensions/machine-learning-services/src/test/index.ts new file mode 100644 index 0000000000..70ad3bb59e --- /dev/null +++ b/extensions/machine-learning-services/src/test/index.ts @@ -0,0 +1,48 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +const path = require('path'); +const testRunner = require('vscode/lib/testrunner'); + +const suite = 'machine learning Extension Tests'; + +const options: any = { + ui: 'bdd', + useColors: true, + timeout: 600000 +}; + +// set relevant mocha options from the environment +if (process.env.ADS_TEST_GREP) { + options.grep = process.env.ADS_TEST_GREP; + console.log(`setting options.grep to: ${options.grep}`); +} +if (process.env.ADS_TEST_INVERT_GREP) { + options.invert = parseInt(process.env.ADS_TEST_INVERT_GREP); + console.log(`setting options.invert to: ${options.invert}`); +} +if (process.env.ADS_TEST_TIMEOUT) { + options.timeout = parseInt(process.env.ADS_TEST_TIMEOUT); + console.log(`setting options.timeout to: ${options.timeout}`); +} +if (process.env.ADS_TEST_RETRIES) { + options.retries = parseInt(process.env.ADS_TEST_RETRIES); + console.log(`setting options.retries to: ${options.retries}`); +} + +if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) { + options.reporter = 'mocha-multi-reporters'; + options.reporterOptions = { + reporterEnabled: 'spec, mocha-junit-reporter', + mochaJunitReporterReporterOptions: { + testsuitesTitle: `${suite} ${process.platform}`, + mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`) + } + }; +} + +testRunner.configure(options); + +export = testRunner; diff --git a/extensions/machine-learning-services/src/test/mainController.test.ts b/extensions/machine-learning-services/src/test/mainController.test.ts new file mode 100644 index 0000000000..f93f9b0e17 --- /dev/null +++ b/extensions/machine-learning-services/src/test/mainController.test.ts @@ -0,0 +1,117 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as vscode from 'vscode'; +import * as should from 'should'; +import 'mocha'; +import * as TypeMoq from 'typemoq'; +import * as path from 'path'; +import { ApiWrapper } from '../common/apiWrapper'; +import { QueryRunner } from '../common/queryRunner'; +import { ProcessService } from '../common/processService'; +import MainController from '../controllers/mainController'; +import { PackageManager } from '../packageManagement/packageManager'; + +interface TestContext { + + apiWrapper: TypeMoq.IMock; + queryRunner: TypeMoq.IMock; + processService: TypeMoq.IMock; + context: vscode.ExtensionContext; + outputChannel: vscode.OutputChannel; + extension: vscode.Extension; + packageManager: TypeMoq.IMock; +} + +function createContext(): TestContext { + let extensionPath = path.join(__dirname, '..', '..'); + return { + apiWrapper: TypeMoq.Mock.ofType(ApiWrapper), + queryRunner: TypeMoq.Mock.ofType(QueryRunner), + processService: TypeMoq.Mock.ofType(ProcessService), + packageManager: TypeMoq.Mock.ofType(PackageManager), + context: { + subscriptions: [], + workspaceState: { + get: () => {return undefined;}, + update: () => {return Promise.resolve();} + }, + globalState: { + get: () => {return Promise.resolve();}, + update: () => {return Promise.resolve();} + }, + extensionPath: extensionPath, + asAbsolutePath: () => {return '';}, + storagePath: '', + globalStoragePath: '', + logPath: '' + }, + outputChannel: { + name: '', + append: () => { }, + appendLine: () => { }, + clear: () => { }, + show: () => { }, + hide: () => { }, + dispose: () => { } + }, + extension: { + id: '', + extensionPath: '', + isActive: true, + packageJSON: {}, + extensionKind: vscode.ExtensionKind.UI, + exports: {}, + activate: () => { return Promise.resolve(); } + } + }; +} + +function createController(testContext: TestContext): MainController { + let controller = new MainController(testContext.context, testContext.apiWrapper.object, testContext.queryRunner.object, testContext.processService.object); + controller.packageManager = testContext.packageManager.object; + return controller; +} + +describe('Main Controller', () => { + it('Should create new instance successfully', async function (): Promise { + let testContext = createContext(); + testContext.apiWrapper.setup(x => x.createOutputChannel(TypeMoq.It.isAny())).returns(() => testContext.outputChannel); + should.doesNotThrow(() => createController(testContext)); + }); + + it('initialize Should install dependencies successfully', async function (): Promise { + let testContext = createContext(); + testContext.apiWrapper.setup(x => x.createOutputChannel(TypeMoq.It.isAny())).returns(() => testContext.outputChannel); + testContext.apiWrapper.setup(x => x.getExtension(TypeMoq.It.isAny())).returns(() => testContext.extension); + testContext.packageManager.setup(x => x.managePackages()).returns(() => Promise.resolve()); + testContext.packageManager.setup(x => x.installDependencies()).returns(() => Promise.resolve()); + testContext.apiWrapper.setup(x => x.registerCommand(TypeMoq.It.isAny(), TypeMoq.It.isAny())); + let controller = createController(testContext); + await controller.activate(); + should.deepEqual(controller.config.requiredPythonPackages, [ + { name: 'pymssql', version: '2.1.4' }, + { name: 'sqlmlutils', version: '' } + ]); + }); + + it('initialize Should show and error in output channel if installing dependencies fails', async function (): Promise { + let errorReported = false; + let testContext = createContext(); + testContext.apiWrapper.setup(x => x.createOutputChannel(TypeMoq.It.isAny())).returns(() => testContext.outputChannel); + testContext.apiWrapper.setup(x => x.getExtension(TypeMoq.It.isAny())).returns(() => testContext.extension); + testContext.packageManager.setup(x => x.managePackages()).returns(() => Promise.resolve()); + testContext.packageManager.setup(x => x.installDependencies()).returns(() => Promise.reject()); + testContext.apiWrapper.setup(x => x.registerCommand(TypeMoq.It.isAny(), TypeMoq.It.isAny())); + testContext.outputChannel.appendLine = () => { + errorReported = true; + }; + let controller = createController(testContext); + await controller.activate(); + should.equal(errorReported, true); + }); +}); diff --git a/extensions/machine-learning-services/src/test/packageManagement/packageManager.test.ts b/extensions/machine-learning-services/src/test/packageManagement/packageManager.test.ts new file mode 100644 index 0000000000..652b417b83 --- /dev/null +++ b/extensions/machine-learning-services/src/test/packageManagement/packageManager.test.ts @@ -0,0 +1,223 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as azdata from 'azdata'; + +import * as should from 'should'; +import 'mocha'; +import * as TypeMoq from 'typemoq'; +import { PackageManager } from '../../packageManagement/packageManager'; +import { SqlPythonPackageManageProvider } from '../../packageManagement/sqlPackageManageProvider'; +import { createContext, TestContext } from './utils'; + +describe('Package Manager', () => { + it('Should initialize SQL package manager successfully', async function (): Promise { + let testContext = createContext(); + should.doesNotThrow(() => createPackageManager(testContext)); + should.equal(testContext.nbExtensionApis.getPackageManagers().has(SqlPythonPackageManageProvider.ProviderId), true); + }); + + it('Manage Package command Should execute the command for valid connection', async function (): Promise { + let testContext = createContext(); + let connection = new azdata.connection.ConnectionProfile(); + testContext.apiWrapper.setup(x => x.getCurrentConnection()).returns(() => {return Promise.resolve(connection);}); + testContext.apiWrapper.setup(x => x.executeCommand(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => {return Promise.resolve();}); + testContext.queryRunner.setup(x => x.isPythonInstalled(connection)).returns(() => {return Promise.resolve(true);}); + let packageManager = createPackageManager(testContext); + await packageManager.managePackages(); + testContext.apiWrapper.verify(x => x.executeCommand(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once()); + }); + + it('Manage Package command Should show an error for connection without python installed', async function (): Promise { + let testContext = createContext(); + let connection = new azdata.connection.ConnectionProfile(); + testContext.apiWrapper.setup(x => x.getCurrentConnection()).returns(() => {return Promise.resolve(connection);}); + testContext.apiWrapper.setup(x => x.executeCommand(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => {return Promise.resolve();}); + testContext.apiWrapper.setup(x => x.showInfoMessage(TypeMoq.It.isAny())); + testContext.queryRunner.setup(x => x.isPythonInstalled(connection)).returns(() => {return Promise.resolve(false);}); + let packageManager = createPackageManager(testContext); + await packageManager.managePackages(); + testContext.apiWrapper.verify(x => x.showInfoMessage(TypeMoq.It.isAny()), TypeMoq.Times.once()); + }); + + it('Manage Package command Should show an error for no connection', async function (): Promise { + let testContext = createContext(); + let connection: azdata.connection.ConnectionProfile; + testContext.apiWrapper.setup(x => x.getCurrentConnection()).returns(() => {return Promise.resolve(connection);}); + testContext.apiWrapper.setup(x => x.executeCommand(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => {return Promise.resolve();}); + testContext.apiWrapper.setup(x => x.showInfoMessage(TypeMoq.It.isAny())); + + let packageManager = createPackageManager(testContext); + await packageManager.managePackages(); + testContext.apiWrapper.verify(x => x.showInfoMessage(TypeMoq.It.isAny()), TypeMoq.Times.once()); + }); + + it('installDependencies Should install python if does not exist', async function (): Promise { + let testContext = createContext(); + let pythonInstalled = false; + let installedPackages = `[ + {"name":"pymssql","version":"2.1.4"}, + {"name":"sqlmlutils","version":"1.1.1"} + ]`; + testContext.apiWrapper.setup(x => x.startBackgroundOperation(TypeMoq.It.isAny())).returns((operationInfo: azdata.BackgroundOperationInfo) => { + operationInfo.operation(testContext.op); + }); + testContext.jupyterInstallation.installPythonPackage = () => { + pythonInstalled = true; + return Promise.resolve(); + }; + testContext.processService.setup(x => x.executeBufferedCommand(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => {return Promise.resolve(installedPackages);}); + + let packageManager = createPackageManager(testContext); + await packageManager.installDependencies(); + should.equal(testContext.getOpStatus(), azdata.TaskStatus.Succeeded); + should.equal(pythonInstalled, true); + }); + + it('installDependencies Should fail the task if installing python fails', async function (): Promise { + let testContext = createContext(); + let installedPackages = `[ + {"name":"pymssql","version":"2.1.4"}, + {"name":"sqlmlutils","version":"1.1.1"} + ]`; + testContext.apiWrapper.setup(x => x.startBackgroundOperation(TypeMoq.It.isAny())).returns((operationInfo: azdata.BackgroundOperationInfo) => { + operationInfo.operation(testContext.op); + }); + testContext.jupyterInstallation.installPythonPackage = () => { + return Promise.reject(); + }; + testContext.processService.setup(x => x.executeBufferedCommand(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => {return Promise.resolve(installedPackages);}); + + let packageManager = createPackageManager(testContext); + await should(packageManager.installDependencies()).rejected(); + should.equal(testContext.getOpStatus(), azdata.TaskStatus.Failed); + }); + + it('installDependencies Should not install packages if already installed', async function (): Promise { + let testContext = createContext(); + let packagesInstalled = false; + let installedPackages = `[ + {"name":"pymssql","version":"2.1.4"}, + {"name":"sqlmlutils","version":"1.1.1"} + ]`; + testContext.apiWrapper.setup(x => x.startBackgroundOperation(TypeMoq.It.isAny())).returns((operationInfo: azdata.BackgroundOperationInfo) => { + operationInfo.operation(testContext.op); + }); + testContext.jupyterInstallation.installPythonPackage = () => { + return Promise.resolve(); + }; + testContext.processService.setup(x => x.executeBufferedCommand(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((command) => { + if (command.indexOf('pip install') > 0) { + packagesInstalled = true; + } + return Promise.resolve(installedPackages); + }); + + let packageManager = createPackageManager(testContext); + await packageManager.installDependencies(); + should.equal(testContext.getOpStatus(), azdata.TaskStatus.Succeeded); + should.equal(packagesInstalled, false); + }); + + it('installDependencies Should install packages that are not already installed', async function (): Promise { + let testContext = createContext(); + let packagesInstalled = false; + let installedPackages = `[ + {"name":"pymssql","version":"2.1.4"} + ]`; + testContext.apiWrapper.setup(x => x.startBackgroundOperation(TypeMoq.It.isAny())).returns((operationInfo: azdata.BackgroundOperationInfo) => { + operationInfo.operation(testContext.op); + }); + testContext.jupyterInstallation.installPythonPackage = () => { + return Promise.resolve(); + }; + testContext.processService.setup(x => x.executeBufferedCommand(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((command) => { + if (command.indexOf('pip install') > 0) { + packagesInstalled = true; + } + return Promise.resolve(installedPackages); + }); + + let packageManager = createPackageManager(testContext); + await packageManager.installDependencies(); + should.equal(testContext.getOpStatus(), azdata.TaskStatus.Succeeded); + should.equal(packagesInstalled, true); + }); + + it('installDependencies Should install packages if list packages fails', async function (): Promise { + let testContext = createContext(); + let packagesInstalled = false; + testContext.apiWrapper.setup(x => x.startBackgroundOperation(TypeMoq.It.isAny())).returns((operationInfo: azdata.BackgroundOperationInfo) => { + operationInfo.operation(testContext.op); + }); + testContext.jupyterInstallation.installPythonPackage = () => { + return Promise.resolve(); + }; + testContext.processService.setup(x => x.executeBufferedCommand(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((command,) => { + if (command.indexOf('pip list') > 0) { + return Promise.reject(); + } else if (command.indexOf('pip install') > 0) { + packagesInstalled = true; + return Promise.resolve(''); + } else { + return Promise.resolve(''); + } + }); + + let packageManager = createPackageManager(testContext); + await packageManager.installDependencies(); + should.equal(testContext.getOpStatus(), azdata.TaskStatus.Succeeded); + should.equal(packagesInstalled, true); + }); + + it('installDependencies Should fail if install packages fails', async function (): Promise { + let testContext = createContext(); + let packagesInstalled = false; + let installedPackages = `[ + {"name":"pymssql","version":"2.1.4"} + ]`; + testContext.apiWrapper.setup(x => x.startBackgroundOperation(TypeMoq.It.isAny())).returns((operationInfo: azdata.BackgroundOperationInfo) => { + operationInfo.operation(testContext.op); + }); + testContext.jupyterInstallation.installPythonPackage = () => { + return Promise.resolve(); + }; + testContext.processService.setup(x => x.executeBufferedCommand(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((command) => { + if (command.indexOf('pip list') > 0) { + return Promise.resolve(installedPackages); + } else if (command.indexOf('pip install') > 0) { + return Promise.reject(); + } else { + return Promise.resolve(''); + } + }); + + let packageManager = createPackageManager(testContext); + await should(packageManager.installDependencies()).rejected(); + should.equal(testContext.getOpStatus(), azdata.TaskStatus.Failed); + should.equal(packagesInstalled, false); + }); + + function createPackageManager(testContext: TestContext): PackageManager { + testContext.config.setup(x => x.requiredPythonPackages).returns( () => [ + { name: 'pymssql', version: '2.1.4' }, + { name: 'sqlmlutils', version: '' } + ]); + let packageManager = new PackageManager( + testContext.nbExtensionApis, + testContext.outputChannel, + '', + testContext.apiWrapper.object, + testContext.queryRunner.object, + testContext.processService.object, + testContext.config.object); + packageManager.init(); + return packageManager; + } +}); + + diff --git a/extensions/machine-learning-services/src/test/packageManagement/sqlPackageManageProvider.test.ts b/extensions/machine-learning-services/src/test/packageManagement/sqlPackageManageProvider.test.ts new file mode 100644 index 0000000000..68c0b8876c --- /dev/null +++ b/extensions/machine-learning-services/src/test/packageManagement/sqlPackageManageProvider.test.ts @@ -0,0 +1,373 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as azdata from 'azdata'; +import * as should from 'should'; +import 'mocha'; +import * as TypeMoq from 'typemoq'; +import * as constants from '../../common/constants'; +import { SqlPythonPackageManageProvider } from '../../packageManagement/sqlPackageManageProvider'; +import { createContext, TestContext } from './utils'; +import * as nbExtensionApis from '../../typings/notebookServices'; + +describe('SQL Package Manager', () => { + it('Should create SQL package manager successfully', async function (): Promise { + let testContext = createContext(); + should.doesNotThrow(() => createProvider(testContext)); + }); + + it('Should return provider Id and target correctly', async function (): Promise { + let testContext = createContext(); + let provider = createProvider(testContext); + should.deepEqual(SqlPythonPackageManageProvider.ProviderId, provider.providerId); + should.deepEqual({ location: 'SQL', packageType: 'Python' }, provider.packageTarget); + }); + + it('listPackages Should return packages sorted by name', async function (): Promise { + let testContext = createContext(); + let packages: nbExtensionApis.IPackageDetails[] = [ + { + 'name': 'b-name', + 'version': '1.1.1' + }, + { + 'name': 'a-name', + 'version': '1.1.2' + } + ]; + + let connection = new azdata.connection.ConnectionProfile(); + testContext.apiWrapper.setup(x => x.getCurrentConnection()).returns(() => { return Promise.resolve(connection); }); + testContext.queryRunner.setup(x => x.getPythonPackages(TypeMoq.It.isAny())).returns(() => Promise.resolve(packages)); + + let provider = createProvider(testContext); + let actual = await provider.listPackages(); + let expected = [ + { + 'name': 'a-name', + 'version': '1.1.2' + }, + { + 'name': 'b-name', + 'version': '1.1.1' + } + ]; + should.deepEqual(actual, expected); + }); + + it('listPackages Should return empty packages if undefined packages returned', async function (): Promise { + let testContext = createContext(); + + let connection = new azdata.connection.ConnectionProfile(); + let packages: nbExtensionApis.IPackageDetails[]; + testContext.apiWrapper.setup(x => x.getCurrentConnection()).returns(() => { return Promise.resolve(connection); }); + testContext.queryRunner.setup(x => x.getPythonPackages(TypeMoq.It.isAny())).returns(() => Promise.resolve(packages)); + + let provider = createProvider(testContext); + let actual = await provider.listPackages(); + let expected: nbExtensionApis.IPackageDetails[] = []; + should.deepEqual(actual, expected); + }); + + it('listPackages Should return empty packages if empty packages returned', async function (): Promise { + let testContext = createContext(); + + let connection = new azdata.connection.ConnectionProfile(); + testContext.apiWrapper.setup(x => x.getCurrentConnection()).returns(() => { return Promise.resolve(connection); }); + testContext.queryRunner.setup(x => x.getPythonPackages(TypeMoq.It.isAny())).returns(() => Promise.resolve([])); + + let provider = createProvider(testContext); + let actual = await provider.listPackages(); + let expected: nbExtensionApis.IPackageDetails[] = []; + should.deepEqual(actual, expected); + }); + + it('installPackages Should install given packages successfully', async function (): Promise { + let testContext = createContext(); + let packagesUpdated = false; + let packages: nbExtensionApis.IPackageDetails[] = [ + { + 'name': 'a-name', + 'version': '1.1.2' + }, + { + 'name': 'b-name', + 'version': '1.1.1' + } + ]; + + let connection = new azdata.connection.ConnectionProfile(); + connection.serverName = 'serverName'; + connection.databaseName = 'databaseName'; + let credentials = { [azdata.ConnectionOptionSpecialType.password]: 'password' }; + testContext.apiWrapper.setup(x => x.getCurrentConnection()).returns(() => { return Promise.resolve(connection); }); + testContext.apiWrapper.setup(x => x.getCredentials(TypeMoq.It.isAny())).returns(() => { return Promise.resolve(credentials); }); + testContext.processService.setup(x => x.execScripts(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((path, scripts: string[]) => { + + if (path && scripts.find(x => x.indexOf('install') > 0) && + scripts.find(x => x.indexOf('port=1433') > 0) && + scripts.find(x => x.indexOf('server="serverName"') > 0) && + scripts.find(x => x.indexOf('database="databaseName"') > 0) && + scripts.find(x => x.indexOf('package="a-name"') > 0) && + scripts.find(x => x.indexOf('version="1.1.2"') > 0) && + scripts.find(x => x.indexOf('pwd="password"') > 0)) { + packagesUpdated = true; + } + + return Promise.resolve(); + }); + + let provider = createProvider(testContext); + await provider.installPackages(packages, false); + + should.deepEqual(packagesUpdated, true); + }); + + it('uninstallPackages Should uninstall given packages successfully', async function (): Promise { + let testContext = createContext(); + let packagesUpdated = false; + let packages: nbExtensionApis.IPackageDetails[] = [ + { + 'name': 'a-name', + 'version': '1.1.2' + }, + { + 'name': 'b-name', + 'version': '1.1.1' + } + ]; + + let connection = new azdata.connection.ConnectionProfile(); + connection.serverName = 'serverName'; + connection.databaseName = 'databaseName'; + let credentials = { [azdata.ConnectionOptionSpecialType.password]: 'password' }; + testContext.apiWrapper.setup(x => x.getCurrentConnection()).returns(() => { return Promise.resolve(connection); }); + testContext.apiWrapper.setup(x => x.getCredentials(TypeMoq.It.isAny())).returns(() => { return Promise.resolve(credentials); }); + testContext.processService.setup(x => x.execScripts(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((path, scripts: string[]) => { + + if (path && scripts.find(x => x.indexOf('uninstall') > 0) && + scripts.find(x => x.indexOf('port=1433') > 0) && + scripts.find(x => x.indexOf('server="serverName"') > 0) && + scripts.find(x => x.indexOf('database="databaseName"') > 0) && + scripts.find(x => x.indexOf('package_name="a-name"') > 0) && + scripts.find(x => x.indexOf('pwd="password"') > 0)) { + packagesUpdated = true; + } + + return Promise.resolve(); + }); + + let provider = createProvider(testContext); + await provider.uninstallPackages(packages); + + should.deepEqual(packagesUpdated, true); + }); + + it('installPackages Should include port name in the script', async function (): Promise { + let testContext = createContext(); + let packagesUpdated = false; + let packages: nbExtensionApis.IPackageDetails[] = [ + { + 'name': 'a-name', + 'version': '1.1.2' + }, + { + 'name': 'b-name', + 'version': '1.1.1' + } + ]; + + let connection = new azdata.connection.ConnectionProfile(); + connection.serverName = 'serverName,3433'; + connection.databaseName = 'databaseName'; + let credentials = { [azdata.ConnectionOptionSpecialType.password]: 'password' }; + testContext.apiWrapper.setup(x => x.getCurrentConnection()).returns(() => { return Promise.resolve(connection); }); + testContext.apiWrapper.setup(x => x.getCredentials(TypeMoq.It.isAny())).returns(() => { return Promise.resolve(credentials); }); + testContext.processService.setup(x => x.execScripts(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((path, scripts: string[]) => { + + if (path && scripts.find(x => x.indexOf('install') > 0) && + scripts.find(x => x.indexOf('port=3433') > 0) && + scripts.find(x => x.indexOf('server="serverName"') > 0) && + scripts.find(x => x.indexOf('database="databaseName"') > 0) && + scripts.find(x => x.indexOf('package="a-name"') > 0) && + scripts.find(x => x.indexOf('version="1.1.2"') > 0) && + scripts.find(x => x.indexOf('pwd="password"') > 0)) { + packagesUpdated = true; + } + + return Promise.resolve(); + }); + + let provider = createProvider(testContext); + await provider.installPackages(packages, false); + + should.deepEqual(packagesUpdated, true); + }); + + it('installPackages Should not install any packages give empty list', async function (): Promise { + let testContext = createContext(); + let packagesUpdated = false; + let packages: nbExtensionApis.IPackageDetails[] = [ + ]; + + let connection = new azdata.connection.ConnectionProfile(); + let credentials = { ['azdata.ConnectionOptionSpecialType.password']: 'password' }; + testContext.apiWrapper.setup(x => x.getCurrentConnection()).returns(() => { return Promise.resolve(connection); }); + testContext.apiWrapper.setup(x => x.getCredentials(TypeMoq.It.isAny())).returns(() => { return Promise.resolve(credentials); }); + testContext.processService.setup(x => x.execScripts(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => { + packagesUpdated = true; + return Promise.resolve(); + }); + + + let provider = createProvider(testContext); + await provider.installPackages(packages, false); + + should.deepEqual(packagesUpdated, false); + }); + + it('uninstallPackages Should not uninstall any packages give empty list', async function (): Promise { + let testContext = createContext(); + let packagesUpdated = false; + let packages: nbExtensionApis.IPackageDetails[] = [ + ]; + + let connection = new azdata.connection.ConnectionProfile(); + let credentials = { ['azdata.ConnectionOptionSpecialType.password']: 'password' }; + testContext.apiWrapper.setup(x => x.getCurrentConnection()).returns(() => { return Promise.resolve(connection); }); + testContext.apiWrapper.setup(x => x.getCredentials(TypeMoq.It.isAny())).returns(() => { return Promise.resolve(credentials); }); + testContext.processService.setup(x => x.execScripts(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => { + packagesUpdated = true; + return Promise.resolve(); + }); + + + let provider = createProvider(testContext); + await provider.uninstallPackages(packages); + + should.deepEqual(packagesUpdated, false); + }); + + it('canUseProvider Should return false for no connection', async function (): Promise { + let testContext = createContext(); + let connection: azdata.connection.ConnectionProfile; + testContext.apiWrapper.setup(x => x.getCurrentConnection()).returns(() => { return Promise.resolve(connection); }); + + let provider = createProvider(testContext); + let actual = await provider.canUseProvider(); + + should.deepEqual(actual, false); + }); + + it('canUseProvider Should return false if connection does not have python installed', async function (): Promise { + let testContext = createContext(); + + let connection = new azdata.connection.ConnectionProfile(); + testContext.apiWrapper.setup(x => x.getCurrentConnection()).returns(() => { return Promise.resolve(connection); }); + testContext.queryRunner.setup(x => x.isPythonInstalled(TypeMoq.It.isAny())).returns(() => Promise.resolve(false)); + + let provider = createProvider(testContext); + let actual = await provider.canUseProvider(); + + should.deepEqual(actual, false); + }); + + it('canUseProvider Should return true if connection has python installed', async function (): Promise { + let testContext = createContext(); + + let connection = new azdata.connection.ConnectionProfile(); + testContext.apiWrapper.setup(x => x.getCurrentConnection()).returns(() => { return Promise.resolve(connection); }); + testContext.queryRunner.setup(x => x.isPythonInstalled(TypeMoq.It.isAny())).returns(() => Promise.resolve(true)); + + let provider = createProvider(testContext); + let actual = await provider.canUseProvider(); + + should.deepEqual(actual, true); + }); + + it('getPackageOverview Should not return undefined if python package provider not found', async function (): Promise { + let testContext = createContext(); + + let provider = createProvider(testContext); + let actual = await provider.getPackageOverview('package name'); + + should.notEqual(actual, undefined); + }); + + it('getPackageOverview Should return package info using python packages provider', async function (): Promise { + let testContext = createContext(); + let packagePreview = { + 'name': 'a-name', + 'versions': ['1.1.2'], + 'summary': '' + }; + let pythonPackageManager: nbExtensionApis.IPackageManageProvider = { + providerId: 'localhost_Pip', + packageTarget: { location: '', packageType: '' }, + listPackages: () => { return Promise.resolve([]); }, + installPackages: () => { return Promise.resolve(); }, + uninstallPackages: () => { return Promise.resolve(); }, + canUseProvider: () => { return Promise.resolve(true); }, + getLocationTitle: () => { return Promise.resolve(''); }, + getPackageOverview: () => { return Promise.resolve(packagePreview); } + }; + testContext.nbExtensionApis.registerPackageManager(pythonPackageManager.providerId, pythonPackageManager); + + let provider = createProvider(testContext); + let actual = await provider.getPackageOverview('package name'); + + should.deepEqual(actual, packagePreview); + }); + + it('getLocationTitle Should default string for no connection', async function (): Promise { + let testContext = createContext(); + let connection: azdata.connection.ConnectionProfile; + testContext.apiWrapper.setup(x => x.getCurrentConnection()).returns(() => { return Promise.resolve(connection); }); + + let provider = createProvider(testContext); + let actual = await provider.getLocationTitle(); + + should.deepEqual(actual, constants.packageManagerNoConnection); + }); + + it('getLocationTitle Should return connection title string for valid connection', async function (): Promise { + let testContext = createContext(); + + let connection = new azdata.connection.ConnectionProfile(); + connection.serverName = 'serverName'; + connection.databaseName = 'databaseName'; + testContext.apiWrapper.setup(x => x.getCurrentConnection()).returns(() => { return Promise.resolve(connection); }); + + let provider = createProvider(testContext); + let actual = await provider.getLocationTitle(); + + should.deepEqual(actual, `${connection.serverName} ${connection.databaseName}`); + }); + + it('getLocationTitle Should return server name as connection title if there is not database name', async function (): Promise { + let testContext = createContext(); + + let connection = new azdata.connection.ConnectionProfile(); + connection.serverName = 'serverName'; + testContext.apiWrapper.setup(x => x.getCurrentConnection()).returns(() => { return Promise.resolve(connection); }); + + let provider = createProvider(testContext); + let actual = await provider.getLocationTitle(); + + should.deepEqual(actual, `${connection.serverName} `); + }); + + function createProvider(testContext: TestContext): SqlPythonPackageManageProvider { + return new SqlPythonPackageManageProvider( + testContext.nbExtensionApis, + testContext.outputChannel, + '', + testContext.apiWrapper.object, + testContext.queryRunner.object, + testContext.processService.object); + } +}); diff --git a/extensions/machine-learning-services/src/test/packageManagement/utils.ts b/extensions/machine-learning-services/src/test/packageManagement/utils.ts new file mode 100644 index 0000000000..78ca79ca26 --- /dev/null +++ b/extensions/machine-learning-services/src/test/packageManagement/utils.ts @@ -0,0 +1,83 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as vscode from 'vscode'; +import * as azdata from 'azdata'; +import * as nbExtensionApis from '../../typings/notebookServices'; +import * as TypeMoq from 'typemoq'; +import { ApiWrapper } from '../../common/apiWrapper'; +import { QueryRunner } from '../../common/queryRunner'; +import { ProcessService } from '../../common/processService'; +import { Config } from '../../common/config'; + +export interface TestContext { + jupyterInstallation: nbExtensionApis.IJupyterServerInstallation; + jupyterController: nbExtensionApis.IJupyterController; + nbExtensionApis: nbExtensionApis.IExtensionApi; + outputChannel: vscode.OutputChannel; + processService: TypeMoq.IMock; + apiWrapper: TypeMoq.IMock; + queryRunner: TypeMoq.IMock; + config: TypeMoq.IMock; + op: azdata.BackgroundOperation; + getOpStatus: () => azdata.TaskStatus; +} + +export function createContext(): TestContext { + let opStatus: azdata.TaskStatus; + let packages = new Map(); + let jupyterInstallation: nbExtensionApis.IJupyterServerInstallation = { + installCondaPackages: () => { return Promise.resolve(); }, + getInstalledPipPackages: () => { return Promise.resolve([]); }, + installPipPackages: () => { return Promise.resolve(); }, + uninstallPipPackages: () => { return Promise.resolve(); }, + uninstallCondaPackages: () => { return Promise.resolve(); }, + executeBufferedCommand: () => { return Promise.resolve(''); }, + executeStreamedCommand: () => { return Promise.resolve(); }, + pythonExecutable: '', + pythonInstallationPath: '', + installPythonPackage: () => { return Promise.resolve(); } + }; + + let jupyterController = { + jupyterInstallation: jupyterInstallation + }; + + return { + + jupyterInstallation: jupyterInstallation, + jupyterController: jupyterController, + nbExtensionApis: { + getJupyterController: () => { return jupyterController; }, + registerPackageManager: (providerId: string, packageManagerProvider: nbExtensionApis.IPackageManageProvider) => { + packages.set(providerId, packageManagerProvider); + }, + getPackageManagers: () => { return packages; }, + }, + outputChannel: { + name: '', + append: () => { }, + appendLine: () => { }, + clear: () => { }, + show: () => { }, + hide: () => { }, + dispose: () => { } + }, + processService: TypeMoq.Mock.ofType(ProcessService), + apiWrapper: TypeMoq.Mock.ofType(ApiWrapper), + queryRunner: TypeMoq.Mock.ofType(QueryRunner), + config: TypeMoq.Mock.ofType(Config), + op: { + updateStatus: (status: azdata.TaskStatus) => { + opStatus = status; + }, + id: '', + onCanceled: new vscode.EventEmitter().event, + }, + getOpStatus: () => { return opStatus; } + }; +} diff --git a/extensions/machine-learning-services/src/test/queryRunner.test.ts b/extensions/machine-learning-services/src/test/queryRunner.test.ts new file mode 100644 index 0000000000..ec9bf45623 --- /dev/null +++ b/extensions/machine-learning-services/src/test/queryRunner.test.ts @@ -0,0 +1,305 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as azdata from 'azdata'; +import { ApiWrapper } from '../common/apiWrapper'; +import * as TypeMoq from 'typemoq'; +import * as should from 'should'; +import { QueryRunner } from '../common/queryRunner'; +import { IPackageDetails } from '../typings/notebookServices'; + +interface TestContext { + + apiWrapper: TypeMoq.IMock; + queryProvider: azdata.QueryProvider; +} + +function createContext(): TestContext { + return { + apiWrapper: TypeMoq.Mock.ofType(ApiWrapper), + queryProvider: { + providerId: '', + cancelQuery: () => {return Promise.reject();}, + runQuery: () => {return Promise.reject();}, + runQueryStatement: () => {return Promise.reject();}, + runQueryString: () => {return Promise.reject();}, + runQueryAndReturn: () => { return Promise.reject(); }, + parseSyntax: () => {return Promise.reject();}, + getQueryRows: () => {return Promise.reject();}, + disposeQuery: () => {return Promise.reject();}, + saveResults: () => {return Promise.reject();}, + setQueryExecutionOptions: () => {return Promise.reject();}, + registerOnQueryComplete: () => {return Promise.reject();}, + registerOnBatchStart: () => {return Promise.reject();}, + registerOnBatchComplete: () => {return Promise.reject();}, + registerOnResultSetAvailable: () => {return Promise.reject();}, + registerOnResultSetUpdated: () => {return Promise.reject();}, + registerOnMessage: () => {return Promise.reject();}, + commitEdit: () => {return Promise.reject();}, + createRow: () => {return Promise.reject();}, + deleteRow: () => {return Promise.reject();}, + disposeEdit: () => {return Promise.reject();}, + initializeEdit: () => {return Promise.reject();}, + revertCell: () => {return Promise.reject();}, + revertRow: () => {return Promise.reject();}, + updateCell: () => {return Promise.reject();}, + getEditRows: () => {return Promise.reject();}, + registerOnEditSessionReady: () => {return Promise.reject();}, + } + }; +} + +describe('Query Runner', () => { + it('getPythonPackages Should return empty list if not provider found', async function (): Promise { + let testContext = createContext(); + let connection = new azdata.connection.ConnectionProfile(); + let queryRunner = new QueryRunner(testContext.apiWrapper.object); + let queryProvider: azdata.QueryProvider; + testContext.apiWrapper.setup(x => x.getProvider(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => queryProvider); + + let actual = await queryRunner.getPythonPackages(connection); + should.deepEqual(actual, []); + }); + + it('getPythonPackages Should return empty list if not provider throws', async function (): Promise { + let testContext = createContext(); + let connection = new azdata.connection.ConnectionProfile(); + let queryRunner = new QueryRunner(testContext.apiWrapper.object); + testContext.queryProvider.runQueryAndReturn = () => { return Promise.reject(); }; + testContext.apiWrapper.setup(x => x.getProvider(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => testContext.queryProvider); + + let actual = await queryRunner.getPythonPackages(connection); + should.deepEqual(actual, []); + }); + + it('getPythonPackages Should return list if provider runs the query successfully', async function (): Promise { + let testContext = createContext(); + let rows: azdata.DbCellValue[][] = [ + [{ + displayValue: 'p1', + isNull: false, + invariantCultureDisplayValue: '' + }, { + displayValue: '1.1.1', + isNull: false, + invariantCultureDisplayValue: '' + }], + [{ + displayValue: 'p2', + isNull: false, + invariantCultureDisplayValue: '' + }, { + displayValue: '1.1.2', + isNull: false, + invariantCultureDisplayValue: '' + }] + ]; + let expected = [ + { + 'name': 'p1', + 'version': '1.1.1' + }, + { + 'name': 'p2', + 'version': '1.1.2' + } + ]; + + let result : azdata.SimpleExecuteResult = { + rowCount: 2, + columnInfo: [], + rows: rows, + }; + let connection = new azdata.connection.ConnectionProfile(); + let queryRunner = new QueryRunner(testContext.apiWrapper.object); + testContext.queryProvider.runQueryAndReturn = () => { return Promise.resolve(result); }; + testContext.apiWrapper.setup(x => x.getProvider(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => testContext.queryProvider); + + let actual = await queryRunner.getPythonPackages(connection); + + should.deepEqual(actual, expected); + }); + + it('getPythonPackages Should return empty list if provider return no rows', async function (): Promise { + let testContext = createContext(); + let rows: azdata.DbCellValue[][] = [ + ]; + let expected: IPackageDetails[] = []; + + let result : azdata.SimpleExecuteResult = { + rowCount: 2, + columnInfo: [], + rows: rows, + }; + let connection = new azdata.connection.ConnectionProfile(); + let queryRunner = new QueryRunner(testContext.apiWrapper.object); + testContext.queryProvider.runQueryAndReturn = () => { return Promise.resolve(result); }; + testContext.apiWrapper.setup(x => x.getProvider(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => testContext.queryProvider); + + let actual = await queryRunner.getPythonPackages(connection); + + should.deepEqual(actual, expected); + }); + + it('updateExternalScriptConfig Should update config successfully', async function (): Promise { + let testContext = createContext(); + let rows: azdata.DbCellValue[][] = [ + ]; + + let result : azdata.SimpleExecuteResult = { + rowCount: 2, + columnInfo: [], + rows: rows, + }; + let connection = new azdata.connection.ConnectionProfile(); + let queryRunner = new QueryRunner(testContext.apiWrapper.object); + testContext.queryProvider.runQueryAndReturn = () => { return Promise.resolve(result); }; + testContext.apiWrapper.setup(x => x.getProvider(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => testContext.queryProvider); + + await should(queryRunner.updateExternalScriptConfig(connection, true)).resolved(); + + }); + + it('isPythonInstalled Should return true is provider returns valid result', async function (): Promise { + let testContext = createContext(); + let rows: azdata.DbCellValue[][] = [ + [{ + displayValue: '1', + isNull: false, + invariantCultureDisplayValue: '' + }] + ]; + let expected = true; + + let result : azdata.SimpleExecuteResult = { + rowCount: 2, + columnInfo: [], + rows: rows, + }; + let connection = new azdata.connection.ConnectionProfile(); + let queryRunner = new QueryRunner(testContext.apiWrapper.object); + testContext.queryProvider.runQueryAndReturn = () => { return Promise.resolve(result); }; + testContext.apiWrapper.setup(x => x.getProvider(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => testContext.queryProvider); + + let actual = await queryRunner.isPythonInstalled(connection); + should.deepEqual(actual, expected); + }); + + it('isPythonInstalled Should return true is provider returns 0 as result', async function (): Promise { + let testContext = createContext(); + let rows: azdata.DbCellValue[][] = [ + [{ + displayValue: '0', + isNull: false, + invariantCultureDisplayValue: '' + }] + ]; + let expected = false; + + let result : azdata.SimpleExecuteResult = { + rowCount: 2, + columnInfo: [], + rows: rows, + }; + let connection = new azdata.connection.ConnectionProfile(); + let queryRunner = new QueryRunner(testContext.apiWrapper.object); + testContext.queryProvider.runQueryAndReturn = () => { return Promise.resolve(result); }; + testContext.apiWrapper.setup(x => x.getProvider(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => testContext.queryProvider); + + let actual = await queryRunner.isPythonInstalled(connection); + should.deepEqual(actual, expected); + }); + + it('isPythonInstalled Should return false is provider returns no result', async function (): Promise { + let testContext = createContext(); + let rows: azdata.DbCellValue[][] = []; + let expected = false; + + let result : azdata.SimpleExecuteResult = { + rowCount: 2, + columnInfo: [], + rows: rows, + }; + let connection = new azdata.connection.ConnectionProfile(); + let queryRunner = new QueryRunner(testContext.apiWrapper.object); + testContext.queryProvider.runQueryAndReturn = () => { return Promise.resolve(result); }; + testContext.apiWrapper.setup(x => x.getProvider(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => testContext.queryProvider); + + let actual = await queryRunner.isPythonInstalled(connection); + should.deepEqual(actual, expected); + }); + + it('isMachineLearningServiceEnabled Should return true is provider returns valid result', async function (): Promise { + let testContext = createContext(); + let rows: azdata.DbCellValue[][] = [ + [{ + displayValue: '1', + isNull: false, + invariantCultureDisplayValue: '' + }] + ]; + let expected = true; + + let result : azdata.SimpleExecuteResult = { + rowCount: 2, + columnInfo: [], + rows: rows, + }; + let connection = new azdata.connection.ConnectionProfile(); + let queryRunner = new QueryRunner(testContext.apiWrapper.object); + testContext.queryProvider.runQueryAndReturn = () => { return Promise.resolve(result); }; + testContext.apiWrapper.setup(x => x.getProvider(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => testContext.queryProvider); + + let actual = await queryRunner.isMachineLearningServiceEnabled(connection); + should.deepEqual(actual, expected); + }); + + it('isMachineLearningServiceEnabled Should return true is provider returns 0 as result', async function (): Promise { + let testContext = createContext(); + let rows: azdata.DbCellValue[][] = [ + [{ + displayValue: '0', + isNull: false, + invariantCultureDisplayValue: '' + }] + ]; + let expected = false; + + let result : azdata.SimpleExecuteResult = { + rowCount: 2, + columnInfo: [], + rows: rows, + }; + let connection = new azdata.connection.ConnectionProfile(); + let queryRunner = new QueryRunner(testContext.apiWrapper.object); + testContext.queryProvider.runQueryAndReturn = () => { return Promise.resolve(result); }; + testContext.apiWrapper.setup(x => x.getProvider(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => testContext.queryProvider); + + let actual = await queryRunner.isMachineLearningServiceEnabled(connection); + should.deepEqual(actual, expected); + }); + + it('isMachineLearningServiceEnabled Should return false is provider returns no result', async function (): Promise { + let testContext = createContext(); + let rows: azdata.DbCellValue[][] = []; + let expected = false; + + let result : azdata.SimpleExecuteResult = { + rowCount: 2, + columnInfo: [], + rows: rows, + }; + let connection = new azdata.connection.ConnectionProfile(); + let queryRunner = new QueryRunner(testContext.apiWrapper.object); + testContext.queryProvider.runQueryAndReturn = () => { return Promise.resolve(result); }; + testContext.apiWrapper.setup(x => x.getProvider(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => testContext.queryProvider); + + let actual = await queryRunner.isMachineLearningServiceEnabled(connection); + should.deepEqual(actual, expected); + }); + +}); diff --git a/extensions/machine-learning-services/src/types.ts b/extensions/machine-learning-services/src/types.ts new file mode 100644 index 0000000000..8c622f5a29 --- /dev/null +++ b/extensions/machine-learning-services/src/types.ts @@ -0,0 +1,23 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +const _typeof = { + undefined: 'undefined' +}; + +/** + * @returns whether the provided parameter is undefined or null. + */ +export function isUndefinedOrNull(obj: any): boolean { + return isUndefined(obj) || obj === null; +} + +/** + * @returns whether the provided parameter is undefined. + */ +export function isUndefined(obj: any): boolean { + return typeof (obj) === _typeof.undefined; +} diff --git a/extensions/machine-learning-services/src/typings/notebookServices.d.ts b/extensions/machine-learning-services/src/typings/notebookServices.d.ts new file mode 100644 index 0000000000..74ad82a49d --- /dev/null +++ b/extensions/machine-learning-services/src/typings/notebookServices.d.ts @@ -0,0 +1,63 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import * as azdata from 'azdata'; + +/** + * The API provided by this extension. + * + * @export + */ +export interface IExtensionApi { + getJupyterController(): IJupyterController; + registerPackageManager(providerId: string, packageManagerProvider: IPackageManageProvider): void + getPackageManagers(): Map +} + +export interface IJupyterController { + jupyterInstallation: IJupyterServerInstallation; +} + +export interface IJupyterServerInstallation { + installPipPackages(packages: IPackageDetails[], useMinVersion: boolean): Promise; + uninstallPipPackages(packages: IPackageDetails[]): Promise; + installCondaPackages(packages: IPackageDetails[], useMinVersion: boolean): Promise; + uninstallCondaPackages(packages: IPackageDetails[]): Promise; + getInstalledPipPackages(): Promise; + pythonExecutable: string; + pythonInstallationPath: string; + executeBufferedCommand(command: string): Promise; + executeStreamedCommand(command: string): Promise; + installPythonPackage(backgroundOperation: azdata.BackgroundOperation, usingExistingPython: boolean, pythonInstallationPath: string, outputChannel: vscode.OutputChannel): Promise; +} + + +export interface IPackageDetails { + name: string; + version: string; +} + +export interface IPackageTarget { + location: string; + packageType: string; +} + +export interface IPackageOverview { + name: string; + versions: string[]; + summary: string; +} + +export interface IPackageManageProvider { + providerId: string; + packageTarget: IPackageTarget; + listPackages(): Promise + installPackages(package: IPackageDetails[], useMinVersion: boolean): Promise; + uninstallPackages(package: IPackageDetails[]): Promise; + canUseProvider(): Promise; + getLocationTitle(): Promise; + getPackageOverview(packageName: string): Promise +} diff --git a/extensions/machine-learning-services/src/typings/ref.d.ts b/extensions/machine-learning-services/src/typings/ref.d.ts new file mode 100644 index 0000000000..4d46be908b --- /dev/null +++ b/extensions/machine-learning-services/src/typings/ref.d.ts @@ -0,0 +1,9 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/// +/// +/// +/// \ No newline at end of file diff --git a/extensions/machine-learning-services/tsconfig.json b/extensions/machine-learning-services/tsconfig.json new file mode 100644 index 0000000000..a1a141fb0a --- /dev/null +++ b/extensions/machine-learning-services/tsconfig.json @@ -0,0 +1,24 @@ +{ + "extends": "../shared.tsconfig.json", + "compileOnSave": true, + "compilerOptions": { + "module": "commonjs", + "target": "es6", + "outDir": "./out", + "lib": [ + "es6", + "es2015.promise" + ], + "sourceMap": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "moduleResolution": "node", + "declaration": false + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/extensions/machine-learning-services/yarn.lock b/extensions/machine-learning-services/yarn.lock new file mode 100644 index 0000000000..bbfa2ce27a --- /dev/null +++ b/extensions/machine-learning-services/yarn.lock @@ -0,0 +1,1649 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@types/mocha@^5.2.5": + version "5.2.6" + resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-5.2.6.tgz#b8622d50557dd155e9f2f634b7d68fd38de5e94b" + integrity sha512-1axi39YdtBI7z957vdqXI4Ac25e7YihYQtJa+Clnxg1zTJEaIRbndt71O3sP4GAMgiAm0pY26/b9BrY4MR/PMw== + +"@types/node@*": + version "12.12.17" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.17.tgz#191b71e7f4c325ee0fb23bc4a996477d92b8c39b" + integrity sha512-Is+l3mcHvs47sKy+afn2O1rV4ldZFU7W8101cNlOd+MRbjM4Onida8jSZnJdTe/0Pcf25g9BNIUsuugmE6puHA== + +"@types/node@^10.14.8": + version "10.14.17" + resolved "https://registry.yarnpkg.com/@types/node/-/node-10.14.17.tgz#b96d4dd3e427382482848948041d3754d40fd5ce" + integrity sha512-p/sGgiPaathCfOtqu2fx5Mu1bcjuP8ALFg4xpGgNkcin7LwRyzUKniEHBKdcE1RPsenq5JVPIpMTJSygLboygQ== + +"@types/uuid@^3.4.5": + version "3.4.6" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-3.4.6.tgz#d2c4c48eb85a757bf2927f75f939942d521e3016" + integrity sha512-cCdlC/1kGEZdEglzOieLDYBxHsvEOIg7kp/2FYyVR9Pxakq+Qf/inL3RKQ+PA8gOlI/NnL+fXmQH12nwcGzsHw== + dependencies: + "@types/node" "*" + +ajv@^6.5.5: + version "6.10.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.0.tgz#90d0d54439da587cd7e843bfb7045f50bd22bdf1" + integrity sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg== + dependencies: + fast-deep-equal "^2.0.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ansi-cyan@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ansi-cyan/-/ansi-cyan-0.1.1.tgz#538ae528af8982f28ae30d86f2f17456d2609873" + integrity sha1-U4rlKK+JgvKK4w2G8vF0VtJgmHM= + dependencies: + ansi-wrap "0.1.0" + +ansi-red@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/ansi-red/-/ansi-red-0.1.1.tgz#8c638f9d1080800a353c9c28c8a81ca4705d946c" + integrity sha1-jGOPnRCAgAo1PJwoyKgcpHBdlGw= + dependencies: + ansi-wrap "0.1.0" + +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= + +ansi-wrap@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/ansi-wrap/-/ansi-wrap-0.1.0.tgz#a82250ddb0015e9a27ca82e82ea603bbfa45efaf" + integrity sha1-qCJQ3bABXponyoLoLqYDu/pF768= + +append-buffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/append-buffer/-/append-buffer-1.0.2.tgz#d8220cf466081525efea50614f3de6514dfa58f1" + integrity sha1-2CIM9GYIFSXv6lBhTz3mUU36WPE= + dependencies: + buffer-equal "^1.0.0" + +arr-diff@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-1.1.0.tgz#687c32758163588fef7de7b36fabe495eb1a399a" + integrity sha1-aHwydYFjWI/vfeezb6vklesaOZo= + dependencies: + arr-flatten "^1.0.1" + array-slice "^0.2.3" + +arr-flatten@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" + integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg== + +arr-union@^2.0.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-2.1.0.tgz#20f9eab5ec70f5c7d215b1077b1c39161d292c7d" + integrity sha1-IPnqtexw9cfSFbEHexw5Fh0pLH0= + +array-differ@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-1.0.0.tgz#eff52e3758249d33be402b8bb8e564bb2b5d4031" + integrity sha1-7/UuN1gknTO+QCuLuOVkuytdQDE= + +array-slice@^0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/array-slice/-/array-slice-0.2.3.tgz#dd3cfb80ed7973a75117cdac69b0b99ec86186f5" + integrity sha1-3Tz7gO15c6dRF82sabC5nshhhvU= + +array-union@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" + integrity sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk= + dependencies: + array-uniq "^1.0.1" + +array-uniq@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" + integrity sha1-r2rId6Jcx/dOBYiUdThY39sk/bY= + +arrify@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" + integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0= + +asn1@~0.2.3: + version "0.2.4" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" + integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== + dependencies: + safer-buffer "~2.1.0" + +assert-plus@1.0.0, assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= + +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= + +aws4@^1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" + integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ== + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + +bcrypt-pbkdf@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" + integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= + dependencies: + tweetnacl "^0.14.3" + +block-stream@*: + version "0.0.9" + resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a" + integrity sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo= + dependencies: + inherits "~2.0.0" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +browser-stdout@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.0.tgz#f351d32969d32fa5d7a5567154263d928ae3bd1f" + integrity sha1-81HTKWnTL6XXpVZxVCY9korjvR8= + +browser-stdout@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" + integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== + +buffer-crc32@~0.2.3: + version "0.2.13" + resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" + integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI= + +buffer-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/buffer-equal/-/buffer-equal-1.0.0.tgz#59616b498304d556abd466966b22eeda3eca5fbe" + integrity sha1-WWFrSYME1Var1GaWayLu2j7KX74= + +buffer-from@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" + integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== + +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= + +charenc@~0.0.1: + version "0.0.2" + resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" + integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc= + +circular-json@^0.3.1: + version "0.3.3" + resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.3.tgz#815c99ea84f6809529d2f45791bdf82711352d66" + integrity sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A== + +clone-buffer@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58" + integrity sha1-4+JbIHrE5wGvch4staFnksrD3Fg= + +clone-stats@^0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-0.0.1.tgz#b88f94a82cf38b8791d58046ea4029ad88ca99d1" + integrity sha1-uI+UqCzzi4eR1YBG6kAprYjKmdE= + +clone-stats@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-1.0.0.tgz#b3782dff8bb5474e18b9b6bf0fdfe782f8777680" + integrity sha1-s3gt/4u1R04Yuba/D9/ngvh3doA= + +clone@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/clone/-/clone-0.2.0.tgz#c6126a90ad4f72dbf5acdb243cc37724fe93fc1f" + integrity sha1-xhJqkK1Pctv1rNskPMN3JP6T/B8= + +clone@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" + integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4= + +clone@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" + integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18= + +cloneable-readable@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/cloneable-readable/-/cloneable-readable-1.1.2.tgz#d591dee4a8f8bc15da43ce97dceeba13d43e2a65" + integrity sha512-Bq6+4t+lbM8vhTs/Bef5c5AdEMtapp/iFb6+s4/Hh9MVTt8OLKH7ZOOZSCT+Ys7hsHvqv0GuMPJ1lnQJVHvxpg== + dependencies: + inherits "^2.0.1" + process-nextick-args "^2.0.0" + readable-stream "^2.3.5" + +combined-stream@^1.0.6, combined-stream@~1.0.6: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +commander@2.11.0: + version "2.11.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563" + integrity sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ== + +commander@2.15.1: + version "2.15.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f" + integrity sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +convert-source-map@^1.5.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.6.0.tgz#51b537a8c43e0f04dec1993bffcdd504e758ac20" + integrity sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A== + dependencies: + safe-buffer "~5.1.1" + +core-util-is@1.0.2, core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= + +crypt@~0.0.1: + version "0.0.2" + resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" + integrity sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs= + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= + dependencies: + assert-plus "^1.0.0" + +debug@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== + dependencies: + ms "2.0.0" + +debug@^2.2.0: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@^3.1.0: + version "3.2.6" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" + integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== + dependencies: + ms "^2.1.1" + +deep-assign@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/deep-assign/-/deep-assign-1.0.0.tgz#b092743be8427dc621ea0067cdec7e70dd19f37b" + integrity sha1-sJJ0O+hCfcYh6gBnzex+cN0Z83s= + dependencies: + is-obj "^1.0.0" + +define-properties@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" + integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== + dependencies: + object-keys "^1.0.12" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= + +diff@3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.3.1.tgz#aa8567a6eed03c531fc89d3f711cd0e5259dec75" + integrity sha512-MKPHZDMB0o6yHyDryUOScqZibp914ksXwAMYMTHj6KO8UeKsRYNJD3oNCKjTqZon+V488P7N/HzXF8t7ZR95ww== + +diff@3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" + integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== + +duplexer@^0.1.1, duplexer@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1" + integrity sha1-rOb/gIwc5mtX0ev5eXessCM0z8E= + +duplexify@^3.6.0: + version "3.7.1" + resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309" + integrity sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g== + dependencies: + end-of-stream "^1.0.0" + inherits "^2.0.1" + readable-stream "^2.0.0" + stream-shift "^1.0.0" + +ecc-jsbn@~0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" + integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= + dependencies: + jsbn "~0.1.0" + safer-buffer "^2.1.0" + +end-of-stream@^1.0.0, end-of-stream@^1.1.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43" + integrity sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q== + dependencies: + once "^1.4.0" + +escape-string-regexp@1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + +event-stream@3.3.4: + version "3.3.4" + resolved "https://registry.yarnpkg.com/event-stream/-/event-stream-3.3.4.tgz#4ab4c9a0f5a54db9338b4c34d86bfce8f4b35571" + integrity sha1-SrTJoPWlTbkzi0w02Gv86PSzVXE= + dependencies: + duplexer "~0.1.1" + from "~0" + map-stream "~0.1.0" + pause-stream "0.0.11" + split "0.3" + stream-combiner "~0.0.4" + through "~2.3.1" + +event-stream@~3.3.4: + version "3.3.5" + resolved "https://registry.yarnpkg.com/event-stream/-/event-stream-3.3.5.tgz#e5dd8989543630d94c6cf4d657120341fa31636b" + integrity sha512-vyibDcu5JL20Me1fP734QBH/kenBGLZap2n0+XXM7mvuUPzJ20Ydqj1aKcIeMdri1p+PU+4yAKugjN8KCVst+g== + dependencies: + duplexer "^0.1.1" + from "^0.1.7" + map-stream "0.0.7" + pause-stream "^0.0.11" + split "^1.0.1" + stream-combiner "^0.2.2" + through "^2.3.8" + +extend-shallow@^1.1.2: + version "1.1.4" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-1.1.4.tgz#19d6bf94dfc09d76ba711f39b872d21ff4dd9071" + integrity sha1-Gda/lN/AnXa6cR85uHLSH/TdkHE= + dependencies: + kind-of "^1.1.0" + +extend@^3.0.0, extend@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +extsprintf@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= + +extsprintf@^1.2.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" + integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= + +fast-deep-equal@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" + integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk= + +fast-json-stable-stringify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" + integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I= + +fd-slicer@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" + integrity sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4= + dependencies: + pend "~1.2.0" + +flush-write-stream@^1.0.2: + version "1.1.1" + resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.1.1.tgz#8dd7d873a1babc207d94ead0c2e0e44276ebf2e8" + integrity sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w== + dependencies: + inherits "^2.0.3" + readable-stream "^2.3.6" + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= + +form-data@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" + integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + +from@^0.1.7, from@~0: + version "0.1.7" + resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe" + integrity sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4= + +fs-mkdirp-stream@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs-mkdirp-stream/-/fs-mkdirp-stream-1.0.0.tgz#0b7815fc3201c6a69e14db98ce098c16935259eb" + integrity sha1-C3gV/DIBxqaeFNuYzgmMFpNSWes= + dependencies: + graceful-fs "^4.1.11" + through2 "^2.0.3" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + +fstream@^1.0.12: + version "1.0.12" + resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.12.tgz#4e8ba8ee2d48be4f7d0de505455548eae5932045" + integrity sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg== + dependencies: + graceful-fs "^4.1.2" + inherits "~2.0.0" + mkdirp ">=0.5 0" + rimraf "2" + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= + dependencies: + assert-plus "^1.0.0" + +glob-parent@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" + integrity sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4= + dependencies: + is-glob "^3.1.0" + path-dirname "^1.0.0" + +glob-stream@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/glob-stream/-/glob-stream-6.1.0.tgz#7045c99413b3eb94888d83ab46d0b404cc7bdde4" + integrity sha1-cEXJlBOz65SIjYOrRtC0BMx73eQ= + dependencies: + extend "^3.0.0" + glob "^7.1.1" + glob-parent "^3.1.0" + is-negated-glob "^1.0.0" + ordered-read-streams "^1.0.0" + pumpify "^1.3.5" + readable-stream "^2.1.5" + remove-trailing-separator "^1.0.1" + to-absolute-glob "^2.0.0" + unique-stream "^2.0.2" + +glob@7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" + integrity sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^7.1.1, glob@^7.1.2, glob@^7.1.3: + version "7.1.4" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255" + integrity sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +graceful-fs@^4.0.0, graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6: + version "4.1.15" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00" + integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA== + +growl@1.10.3: + version "1.10.3" + resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.3.tgz#1926ba90cf3edfe2adb4927f5880bc22c66c790f" + integrity sha512-hKlsbA5Vu3xsh1Cg3J7jSmX/WaW6A5oBeqzM88oNbCRQFz+zUaXm6yxS4RVytp1scBoJzSYl4YAEOQIt6O8V1Q== + +growl@1.10.5: + version "1.10.5" + resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" + integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA== + +gulp-chmod@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/gulp-chmod/-/gulp-chmod-2.0.0.tgz#00c390b928a0799b251accf631aa09e01cc6299c" + integrity sha1-AMOQuSigeZslGsz2MaoJ4BzGKZw= + dependencies: + deep-assign "^1.0.0" + stat-mode "^0.2.0" + through2 "^2.0.0" + +gulp-filter@^5.0.1: + version "5.1.0" + resolved "https://registry.yarnpkg.com/gulp-filter/-/gulp-filter-5.1.0.tgz#a05e11affb07cf7dcf41a7de1cb7b63ac3783e73" + integrity sha1-oF4Rr/sHz33PQafeHLe2OsN4PnM= + dependencies: + multimatch "^2.0.0" + plugin-error "^0.1.2" + streamfilter "^1.0.5" + +gulp-gunzip@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/gulp-gunzip/-/gulp-gunzip-1.0.0.tgz#15b741145e83a9c6f50886241b57cc5871f151a9" + integrity sha1-FbdBFF6Dqcb1CIYkG1fMWHHxUak= + dependencies: + through2 "~0.6.5" + vinyl "~0.4.6" + +gulp-remote-src-vscode@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/gulp-remote-src-vscode/-/gulp-remote-src-vscode-0.5.1.tgz#a528509457affff3ff30cc73a4a97afe31c41c1d" + integrity sha512-mw4OGjtC/jlCWJFhbcAlel4YPvccChlpsl3JceNiB/DLJi24/UPxXt53/N26lgI3dknEqd4ErfdHrO8sJ5bATQ== + dependencies: + event-stream "3.3.4" + node.extend "^1.1.2" + request "^2.79.0" + through2 "^2.0.3" + vinyl "^2.0.1" + +gulp-untar@^0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/gulp-untar/-/gulp-untar-0.0.7.tgz#92067d79e0fa1e92d60562a100233a44a5aa08b4" + integrity sha512-0QfbCH2a1k2qkTLWPqTX+QO4qNsHn3kC546YhAP3/n0h+nvtyGITDuDrYBMDZeW4WnFijmkOvBWa5HshTic1tw== + dependencies: + event-stream "~3.3.4" + streamifier "~0.1.1" + tar "^2.2.1" + through2 "~2.0.3" + vinyl "^1.2.0" + +gulp-vinyl-zip@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/gulp-vinyl-zip/-/gulp-vinyl-zip-2.1.2.tgz#b79cc1a0e2c3b158ffee294590ade1e9caaf5e7b" + integrity sha512-wJn09jsb8PyvUeyFF7y7ImEJqJwYy40BqL9GKfJs6UGpaGW9A+N68Q+ajsIpb9AeR6lAdjMbIdDPclIGo1/b7Q== + dependencies: + event-stream "3.3.4" + queue "^4.2.1" + through2 "^2.0.3" + vinyl "^2.0.2" + vinyl-fs "^3.0.3" + yauzl "^2.2.1" + yazl "^2.2.1" + +har-schema@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" + integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= + +har-validator@~5.1.0: + version "5.1.3" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080" + integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g== + dependencies: + ajv "^6.5.5" + har-schema "^2.0.0" + +has-flag@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" + integrity sha1-6CB68cx7MNRGzHC3NLXovhj4jVE= + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + +has-symbols@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44" + integrity sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q= + +has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +he@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" + integrity sha1-k0EP0hsAlzUVH4howvJx80J+I/0= + +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" + integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= + dependencies: + assert-plus "^1.0.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= + +is-absolute@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-absolute/-/is-absolute-1.0.0.tgz#395e1ae84b11f26ad1795e73c17378e48a301576" + integrity sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA== + dependencies: + is-relative "^1.0.0" + is-windows "^1.0.1" + +is-buffer@^1.1.5, is-buffer@~1.1.1: + version "1.1.6" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== + +is-extglob@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= + +is-glob@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" + integrity sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo= + dependencies: + is-extglob "^2.1.0" + +is-negated-glob@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-negated-glob/-/is-negated-glob-1.0.0.tgz#6910bca5da8c95e784b5751b976cf5a10fee36d2" + integrity sha1-aRC8pdqMleeEtXUbl2z1oQ/uNtI= + +is-obj@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" + integrity sha1-PkcprB9f3gJc19g6iW2rn09n2w8= + +is-relative@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-relative/-/is-relative-1.0.0.tgz#a1bb6935ce8c5dba1e8b9754b9b2dcc020e2260d" + integrity sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA== + dependencies: + is-unc-path "^1.0.0" + +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= + +is-unc-path@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-unc-path/-/is-unc-path-1.0.0.tgz#d731e8898ed090a12c352ad2eaed5095ad322c9d" + integrity sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ== + dependencies: + unc-path-regex "^0.1.2" + +is-utf8@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" + integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI= + +is-valid-glob@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-valid-glob/-/is-valid-glob-1.0.0.tgz#29bf3eff701be2d4d315dbacc39bc39fe8f601aa" + integrity sha1-Kb8+/3Ab4tTTFdusw5vDn+j2Aao= + +is-windows@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" + integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== + +is@^3.2.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/is/-/is-3.3.0.tgz#61cff6dd3c4193db94a3d62582072b44e5645d79" + integrity sha512-nW24QBoPcFGGHJGUwnfpI7Yc5CdqWNdsyHQszVE/z2pKHXzh7FZ5GWhJqSyaQ9wMkQnsTx+kAI8bHlCX4tKdbg== + +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema@0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" + integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= + +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= + +jsprim@^1.2.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" + integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.2.3" + verror "1.10.0" + +kind-of@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-1.1.0.tgz#140a3d2d41a36d2efcfa9377b62c24f8495a5c44" + integrity sha1-FAo9LUGjbS78+pN3tiwk+ElaXEQ= + +lazystream@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.0.tgz#f6995fe0f820392f61396be89462407bb77168e4" + integrity sha1-9plf4PggOS9hOWvolGJAe7dxaOQ= + dependencies: + readable-stream "^2.0.5" + +lead@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/lead/-/lead-1.0.0.tgz#6f14f99a37be3a9dd784f5495690e5903466ee42" + integrity sha1-bxT5mje+Op3XhPVJVpDlkDRm7kI= + dependencies: + flush-write-stream "^1.0.2" + +lodash@^4.16.4: + version "4.17.15" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" + integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== + +lodash@^4.17.4: + version "4.17.11" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" + integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg== + +map-stream@0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.0.7.tgz#8a1f07896d82b10926bd3744a2420009f88974a8" + integrity sha1-ih8HiW2CsQkmvTdEokIACfiJdKg= + +map-stream@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.1.0.tgz#e56aa94c4c8055a16404a0674b78f215f7c8e194" + integrity sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ= + +md5@^2.1.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/md5/-/md5-2.2.1.tgz#53ab38d5fe3c8891ba465329ea23fac0540126f9" + integrity sha1-U6s41f48iJG6RlMp6iP6wFQBJvk= + dependencies: + charenc "~0.0.1" + crypt "~0.0.1" + is-buffer "~1.1.1" + +mime-db@1.40.0: + version "1.40.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.40.0.tgz#a65057e998db090f732a68f6c276d387d4126c32" + integrity sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA== + +mime-types@^2.1.12, mime-types@~2.1.19: + version "2.1.24" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.24.tgz#b6f8d0b3e951efb77dedeca194cff6d16f676f81" + integrity sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ== + dependencies: + mime-db "1.40.0" + +minimatch@3.0.4, minimatch@^3.0.0, minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + +minimist@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= + +mkdirp@0.5.1, "mkdirp@>=0.5 0", mkdirp@~0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= + dependencies: + minimist "0.0.8" + +mocha-junit-reporter@^1.17.0: + version "1.23.1" + resolved "https://registry.yarnpkg.com/mocha-junit-reporter/-/mocha-junit-reporter-1.23.1.tgz#ba11519c0b967f404e4123dd69bc4ba022ab0f12" + integrity sha512-qeDvKlZyAH2YJE1vhryvjUQ06t2hcnwwu4k5Ddwn0GQINhgEYFhlGM0DwYCVUHq5cuo32qAW6HDsTHt7zz99Ng== + dependencies: + debug "^2.2.0" + md5 "^2.1.0" + mkdirp "~0.5.1" + strip-ansi "^4.0.0" + xml "^1.0.0" + +mocha-multi-reporters@^1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/mocha-multi-reporters/-/mocha-multi-reporters-1.1.7.tgz#cc7f3f4d32f478520941d852abb64d9988587d82" + integrity sha1-zH8/TTL0eFIJQdhSq7ZNmYhYfYI= + dependencies: + debug "^3.1.0" + lodash "^4.16.4" + +mocha@^4.0.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-4.1.0.tgz#7d86cfbcf35cb829e2754c32e17355ec05338794" + integrity sha512-0RVnjg1HJsXY2YFDoTNzcc1NKhYuXKRrBAG2gDygmJJA136Cs2QlRliZG1mA0ap7cuaT30mw16luAeln+4RiNA== + dependencies: + browser-stdout "1.3.0" + commander "2.11.0" + debug "3.1.0" + diff "3.3.1" + escape-string-regexp "1.0.5" + glob "7.1.2" + growl "1.10.3" + he "1.1.1" + mkdirp "0.5.1" + supports-color "4.4.0" + +mocha@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-5.2.0.tgz#6d8ae508f59167f940f2b5b3c4a612ae50c90ae6" + integrity sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ== + dependencies: + browser-stdout "1.3.1" + commander "2.15.1" + debug "3.1.0" + diff "3.5.0" + escape-string-regexp "1.0.5" + glob "7.1.2" + growl "1.10.5" + he "1.1.1" + minimatch "3.0.4" + mkdirp "0.5.1" + supports-color "5.4.0" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + +ms@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +multimatch@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/multimatch/-/multimatch-2.1.0.tgz#9c7906a22fb4c02919e2f5f75161b4cdbd4b2a2b" + integrity sha1-nHkGoi+0wCkZ4vX3UWG0zb1LKis= + dependencies: + array-differ "^1.0.0" + array-union "^1.0.1" + arrify "^1.0.0" + minimatch "^3.0.0" + +node.extend@^1.1.2: + version "1.1.8" + resolved "https://registry.yarnpkg.com/node.extend/-/node.extend-1.1.8.tgz#0aab3e63789f4e6d68b42bc00073ad1881243cf0" + integrity sha512-L/dvEBwyg3UowwqOUTyDsGBU6kjBQOpOhshio9V3i3BMPv5YUb9+mWNN8MK0IbWqT0AqaTSONZf0aTuMMahWgA== + dependencies: + has "^1.0.3" + is "^3.2.1" + +normalize-path@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" + integrity sha1-GrKLVW4Zg2Oowab35vogE3/mrtk= + dependencies: + remove-trailing-separator "^1.0.1" + +now-and-later@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/now-and-later/-/now-and-later-2.0.1.tgz#8e579c8685764a7cc02cb680380e94f43ccb1f7c" + integrity sha512-KGvQ0cB70AQfg107Xvs/Fbu+dGmZoTRJp2TaPwcwQm3/7PteUyN2BCgk8KBMPGBUXZdVwyWS8fDCGFygBm19UQ== + dependencies: + once "^1.3.2" + +oauth-sign@~0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" + integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== + +object-keys@^1.0.11, object-keys@^1.0.12: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object.assign@^4.0.4: + version "4.1.0" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da" + integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w== + dependencies: + define-properties "^1.1.2" + function-bind "^1.1.1" + has-symbols "^1.0.0" + object-keys "^1.0.11" + +once@^1.3.0, once@^1.3.1, once@^1.3.2, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +ordered-read-streams@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz#77c0cb37c41525d64166d990ffad7ec6a0e1363e" + integrity sha1-d8DLN8QVJdZBZtmQ/61+xqDhNj4= + dependencies: + readable-stream "^2.0.1" + +path-dirname@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" + integrity sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA= + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + +pause-stream@0.0.11, pause-stream@^0.0.11: + version "0.0.11" + resolved "https://registry.yarnpkg.com/pause-stream/-/pause-stream-0.0.11.tgz#fe5a34b0cbce12b5aa6a2b403ee2e73b602f1445" + integrity sha1-/lo0sMvOErWqaitAPuLnO2AvFEU= + dependencies: + through "~2.3" + +pend@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" + integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA= + +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= + +plugin-error@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/plugin-error/-/plugin-error-0.1.2.tgz#3b9bb3335ccf00f425e07437e19276967da47ace" + integrity sha1-O5uzM1zPAPQl4HQ34ZJ2ln2kes4= + dependencies: + ansi-cyan "^0.1.1" + ansi-red "^0.1.1" + arr-diff "^1.0.1" + arr-union "^2.0.1" + extend-shallow "^1.1.2" + +postinstall-build@^5.0.1: + version "5.0.3" + resolved "https://registry.yarnpkg.com/postinstall-build/-/postinstall-build-5.0.3.tgz#238692f712a481d8f5bc8960e94786036241efc7" + integrity sha512-vPvPe8TKgp4FLgY3+DfxCE5PIfoXBK2lyLfNCxsRbDsV6vS4oU5RG/IWxrblMn6heagbnMED3MemUQllQ2bQUg== + +process-nextick-args@^2.0.0, process-nextick-args@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" + integrity sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw== + +psl@^1.1.24: + version "1.1.31" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.31.tgz#e9aa86d0101b5b105cbe93ac6b784cd547276184" + integrity sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw== + +pump@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909" + integrity sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +pumpify@^1.3.5: + version "1.5.1" + resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.5.1.tgz#36513be246ab27570b1a374a5ce278bfd74370ce" + integrity sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ== + dependencies: + duplexify "^3.6.0" + inherits "^2.0.3" + pump "^2.0.0" + +punycode@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + integrity sha1-wNWmOycYgArY4esPpSachN1BhF4= + +punycode@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +qs@~6.5.2: + version "6.5.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" + integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== + +querystringify@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.1.tgz#60e5a5fd64a7f8bfa4d2ab2ed6fdf4c85bad154e" + integrity sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA== + +queue@^4.2.1: + version "4.5.1" + resolved "https://registry.yarnpkg.com/queue/-/queue-4.5.1.tgz#6e4290a2d7e99dc75b34494431633fe5437b0dac" + integrity sha512-AMD7w5hRXcFSb8s9u38acBZ+309u6GsiibP4/0YacJeaurRshogB7v/ZcVPxP5gD5+zIw6ixRHdutiYUJfwKHw== + dependencies: + inherits "~2.0.0" + +"readable-stream@>=1.0.33-1 <1.1.0-0": + version "1.0.34" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" + integrity sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw= + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + +readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.1.5, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6: + version "2.3.6" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" + integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +remove-bom-buffer@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/remove-bom-buffer/-/remove-bom-buffer-3.0.0.tgz#c2bf1e377520d324f623892e33c10cac2c252b53" + integrity sha512-8v2rWhaakv18qcvNeli2mZ/TMTL2nEyAKRvzo1WtnZBl15SHyEhrCu2/xKlJyUFKHiHgfXIyuY6g2dObJJycXQ== + dependencies: + is-buffer "^1.1.5" + is-utf8 "^0.2.1" + +remove-bom-stream@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/remove-bom-stream/-/remove-bom-stream-1.2.0.tgz#05f1a593f16e42e1fb90ebf59de8e569525f9523" + integrity sha1-BfGlk/FuQuH7kOv1nejlaVJflSM= + dependencies: + remove-bom-buffer "^3.0.0" + safe-buffer "^5.1.0" + through2 "^2.0.3" + +remove-trailing-separator@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" + integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8= + +replace-ext@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-0.0.1.tgz#29bbd92078a739f0bcce2b4ee41e837953522924" + integrity sha1-KbvZIHinOfC8zitO5B6DeVNSKSQ= + +replace-ext@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.0.tgz#de63128373fcbf7c3ccfa4de5a480c45a67958eb" + integrity sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs= + +request@^2.79.0, request@^2.88.0: + version "2.88.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" + integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg== + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.8.0" + caseless "~0.12.0" + combined-stream "~1.0.6" + extend "~3.0.2" + forever-agent "~0.6.1" + form-data "~2.3.2" + har-validator "~5.1.0" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.19" + oauth-sign "~0.9.0" + performance-now "^2.1.0" + qs "~6.5.2" + safe-buffer "^5.1.2" + tough-cookie "~2.4.3" + tunnel-agent "^0.6.0" + uuid "^3.3.2" + +requires-port@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= + +resolve-options@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/resolve-options/-/resolve-options-1.1.0.tgz#32bb9e39c06d67338dc9378c0d6d6074566ad131" + integrity sha1-MrueOcBtZzONyTeMDW1gdFZq0TE= + dependencies: + value-or-function "^3.0.0" + +rimraf@2: + version "2.6.3" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" + integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== + dependencies: + glob "^7.1.3" + +safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +semver@^5.4.1: + version "5.7.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.0.tgz#790a7cf6fea5459bac96110b29b60412dc8ff96b" + integrity sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA== + +should-equal@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/should-equal/-/should-equal-2.0.0.tgz#6072cf83047360867e68e98b09d71143d04ee0c3" + integrity sha512-ZP36TMrK9euEuWQYBig9W55WPC7uo37qzAEmbjHz4gfyuXrEUgF8cUvQVO+w+d3OMfPvSRQJ22lSm8MQJ43LTA== + dependencies: + should-type "^1.4.0" + +should-format@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/should-format/-/should-format-3.0.3.tgz#9bfc8f74fa39205c53d38c34d717303e277124f1" + integrity sha1-m/yPdPo5IFxT04w01xcwPidxJPE= + dependencies: + should-type "^1.3.0" + should-type-adaptors "^1.0.1" + +should-type-adaptors@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/should-type-adaptors/-/should-type-adaptors-1.1.0.tgz#401e7f33b5533033944d5cd8bf2b65027792e27a" + integrity sha512-JA4hdoLnN+kebEp2Vs8eBe9g7uy0zbRo+RMcU0EsNy+R+k049Ki+N5tT5Jagst2g7EAja+euFuoXFCa8vIklfA== + dependencies: + should-type "^1.3.0" + should-util "^1.0.0" + +should-type@^1.3.0, should-type@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/should-type/-/should-type-1.4.0.tgz#0756d8ce846dfd09843a6947719dfa0d4cff5cf3" + integrity sha1-B1bYzoRt/QmEOmlHcZ36DUz/XPM= + +should-util@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/should-util/-/should-util-1.0.0.tgz#c98cda374aa6b190df8ba87c9889c2b4db620063" + integrity sha1-yYzaN0qmsZDfi6h8mInCtNtiAGM= + +should@^13.2.1: + version "13.2.3" + resolved "https://registry.yarnpkg.com/should/-/should-13.2.3.tgz#96d8e5acf3e97b49d89b51feaa5ae8d07ef58f10" + integrity sha512-ggLesLtu2xp+ZxI+ysJTmNjh2U0TsC+rQ/pfED9bUZZ4DKefP27D+7YJVVTvKsmjLpIi9jAa7itwDGkDDmt1GQ== + dependencies: + should-equal "^2.0.0" + should-format "^3.0.3" + should-type "^1.4.0" + should-type-adaptors "^1.0.1" + should-util "^1.0.0" + +source-map-support@^0.5.0: + version "0.5.12" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.12.tgz#b4f3b10d51857a5af0138d3ce8003b201613d599" + integrity sha512-4h2Pbvyy15EE02G+JOZpUCmqWJuqrs+sEkzewTm++BPi7Hvn/HwcqLAcNxYAyI0x13CpPPn+kMjl+hplXMHITQ== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +split@0.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/split/-/split-0.3.3.tgz#cd0eea5e63a211dfff7eb0f091c4133e2d0dd28f" + integrity sha1-zQ7qXmOiEd//frDwkcQTPi0N0o8= + dependencies: + through "2" + +split@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/split/-/split-1.0.1.tgz#605bd9be303aa59fb35f9229fbea0ddec9ea07d9" + integrity sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg== + dependencies: + through "2" + +sshpk@^1.7.0: + version "1.16.1" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" + integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + bcrypt-pbkdf "^1.0.0" + dashdash "^1.12.0" + ecc-jsbn "~0.1.1" + getpass "^0.1.1" + jsbn "~0.1.0" + safer-buffer "^2.0.2" + tweetnacl "~0.14.0" + +stat-mode@^0.2.0: + version "0.2.2" + resolved "https://registry.yarnpkg.com/stat-mode/-/stat-mode-0.2.2.tgz#e6c80b623123d7d80cf132ce538f346289072502" + integrity sha1-5sgLYjEj19gM8TLOU480YokHJQI= + +stream-combiner@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/stream-combiner/-/stream-combiner-0.2.2.tgz#aec8cbac177b56b6f4fa479ced8c1912cee52858" + integrity sha1-rsjLrBd7Vrb0+kec7YwZEs7lKFg= + dependencies: + duplexer "~0.1.1" + through "~2.3.4" + +stream-combiner@~0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/stream-combiner/-/stream-combiner-0.0.4.tgz#4d5e433c185261dde623ca3f44c586bcf5c4ad14" + integrity sha1-TV5DPBhSYd3mI8o/RMWGvPXErRQ= + dependencies: + duplexer "~0.1.1" + +stream-shift@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952" + integrity sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI= + +streamfilter@^1.0.5: + version "1.0.7" + resolved "https://registry.yarnpkg.com/streamfilter/-/streamfilter-1.0.7.tgz#ae3e64522aa5a35c061fd17f67620c7653c643c9" + integrity sha512-Gk6KZM+yNA1JpW0KzlZIhjo3EaBJDkYfXtYSbOwNIQ7Zd6006E6+sCFlW1NDvFG/vnXhKmw6TJJgiEQg/8lXfQ== + dependencies: + readable-stream "^2.0.2" + +streamifier@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/streamifier/-/streamifier-0.1.1.tgz#97e98d8fa4d105d62a2691d1dc07e820db8dfc4f" + integrity sha1-l+mNj6TRBdYqJpHR3AfoINuN/E8= + +string_decoder@~0.10.x: + version "0.10.31" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ= + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= + dependencies: + ansi-regex "^3.0.0" + +supports-color@4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.4.0.tgz#883f7ddabc165142b2a61427f3352ded195d1a3e" + integrity sha512-rKC3+DyXWgK0ZLKwmRsrkyHVZAjNkfzeehuFWdGGcqGDTZFH73+RH6S/RDAAxl9GusSjZSUWYLmT9N5pzXFOXQ== + dependencies: + has-flag "^2.0.0" + +supports-color@5.4.0: + version "5.4.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.4.0.tgz#1c6b337402c2137605efe19f10fec390f6faab54" + integrity sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w== + dependencies: + has-flag "^3.0.0" + +tar@^2.2.1: + version "2.2.2" + resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.2.tgz#0ca8848562c7299b8b446ff6a4d60cdbb23edc40" + integrity sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA== + dependencies: + block-stream "*" + fstream "^1.0.12" + inherits "2" + +through2-filter@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/through2-filter/-/through2-filter-3.0.0.tgz#700e786df2367c2c88cd8aa5be4cf9c1e7831254" + integrity sha512-jaRjI2WxN3W1V8/FMZ9HKIBXixtiqs3SQSX4/YGIiP3gL6djW48VoZq9tDqeCWs3MT8YY5wb/zli8VW8snY1CA== + dependencies: + through2 "~2.0.0" + xtend "~4.0.0" + +through2@^2.0.0, through2@^2.0.3, through2@~2.0.0, through2@~2.0.3: + version "2.0.5" + resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" + integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ== + dependencies: + readable-stream "~2.3.6" + xtend "~4.0.1" + +through2@~0.6.5: + version "0.6.5" + resolved "https://registry.yarnpkg.com/through2/-/through2-0.6.5.tgz#41ab9c67b29d57209071410e1d7a7a968cd3ad48" + integrity sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg= + dependencies: + readable-stream ">=1.0.33-1 <1.1.0-0" + xtend ">=4.0.0 <4.1.0-0" + +through@2, through@^2.3.8, through@~2.3, through@~2.3.1, through@~2.3.4: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= + +to-absolute-glob@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz#1865f43d9e74b0822db9f145b78cff7d0f7c849b" + integrity sha1-GGX0PZ50sIItufFFt4z/fQ98hJs= + dependencies: + is-absolute "^1.0.0" + is-negated-glob "^1.0.0" + +to-through@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-through/-/to-through-2.0.0.tgz#fc92adaba072647bc0b67d6b03664aa195093af6" + integrity sha1-/JKtq6ByZHvAtn1rA2ZKoZUJOvY= + dependencies: + through2 "^2.0.3" + +tough-cookie@~2.4.3: + version "2.4.3" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781" + integrity sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ== + dependencies: + psl "^1.1.24" + punycode "^1.4.1" + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= + dependencies: + safe-buffer "^5.0.1" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= + +typemoq@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/typemoq/-/typemoq-2.1.0.tgz#4452ce360d92cf2a1a180f0c29de2803f87af1e8" + integrity sha512-DtRNLb7x8yCTv/KHlwes+NI+aGb4Vl1iPC63Hhtcvk1DpxSAZzKWQv0RQFY0jX2Uqj0SDBNl8Na4e6MV6TNDgw== + dependencies: + circular-json "^0.3.1" + lodash "^4.17.4" + postinstall-build "^5.0.1" + +unc-path-regex@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" + integrity sha1-5z3T17DXxe2G+6xrCufYxqadUPo= + +unique-stream@^2.0.2: + version "2.3.1" + resolved "https://registry.yarnpkg.com/unique-stream/-/unique-stream-2.3.1.tgz#c65d110e9a4adf9a6c5948b28053d9a8d04cbeac" + integrity sha512-2nY4TnBE70yoxHkDli7DMazpWiP7xMdCYqU2nBRO0UB+ZpEkGsSija7MvmvnZFUeC+mrgiUfcHSr3LmRFIg4+A== + dependencies: + json-stable-stringify-without-jsonify "^1.0.1" + through2-filter "^3.0.0" + +uri-js@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" + integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== + dependencies: + punycode "^2.1.0" + +url-parse@^1.4.3: + version "1.4.7" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.7.tgz#a8a83535e8c00a316e403a5db4ac1b9b853ae278" + integrity sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg== + dependencies: + querystringify "^2.1.1" + requires-port "^1.0.0" + +util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + +uuid@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" + integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== + +value-or-function@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/value-or-function/-/value-or-function-3.0.0.tgz#1c243a50b595c1be54a754bfece8563b9ff8d813" + integrity sha1-HCQ6ULWVwb5Up1S/7OhWO5/42BM= + +verror@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + +vinyl-fs@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/vinyl-fs/-/vinyl-fs-3.0.3.tgz#c85849405f67428feabbbd5c5dbdd64f47d31bc7" + integrity sha512-vIu34EkyNyJxmP0jscNzWBSygh7VWhqun6RmqVfXePrOwi9lhvRs//dOaGOTRUQr4tx7/zd26Tk5WeSVZitgng== + dependencies: + fs-mkdirp-stream "^1.0.0" + glob-stream "^6.1.0" + graceful-fs "^4.0.0" + is-valid-glob "^1.0.0" + lazystream "^1.0.0" + lead "^1.0.0" + object.assign "^4.0.4" + pumpify "^1.3.5" + readable-stream "^2.3.3" + remove-bom-buffer "^3.0.0" + remove-bom-stream "^1.2.0" + resolve-options "^1.1.0" + through2 "^2.0.0" + to-through "^2.0.0" + value-or-function "^3.0.0" + vinyl "^2.0.0" + vinyl-sourcemap "^1.1.0" + +vinyl-source-stream@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vinyl-source-stream/-/vinyl-source-stream-1.1.2.tgz#62b53a135610a896e98ca96bee3a87f008a8e780" + integrity sha1-YrU6E1YQqJbpjKlr7jqH8Aio54A= + dependencies: + through2 "^2.0.3" + vinyl "^0.4.3" + +vinyl-sourcemap@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/vinyl-sourcemap/-/vinyl-sourcemap-1.1.0.tgz#92a800593a38703a8cdb11d8b300ad4be63b3e16" + integrity sha1-kqgAWTo4cDqM2xHYswCtS+Y7PhY= + dependencies: + append-buffer "^1.0.2" + convert-source-map "^1.5.0" + graceful-fs "^4.1.6" + normalize-path "^2.1.1" + now-and-later "^2.0.0" + remove-bom-buffer "^3.0.0" + vinyl "^2.0.0" + +vinyl@^0.4.3, vinyl@~0.4.6: + version "0.4.6" + resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-0.4.6.tgz#2f356c87a550a255461f36bbeb2a5ba8bf784847" + integrity sha1-LzVsh6VQolVGHza76ypbqL94SEc= + dependencies: + clone "^0.2.0" + clone-stats "^0.0.1" + +vinyl@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-1.2.0.tgz#5c88036cf565e5df05558bfc911f8656df218884" + integrity sha1-XIgDbPVl5d8FVYv8kR+GVt8hiIQ= + dependencies: + clone "^1.0.0" + clone-stats "^0.0.1" + replace-ext "0.0.1" + +vinyl@^2.0.0, vinyl@^2.0.1, vinyl@^2.0.2: + version "2.2.0" + resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-2.2.0.tgz#d85b07da96e458d25b2ffe19fece9f2caa13ed86" + integrity sha512-MBH+yP0kC/GQ5GwBqrTPTzEfiiLjta7hTtvQtbxBgTeSXsmKQRQecjibMbxIXzVT3Y9KJK+drOz1/k+vsu8Nkg== + dependencies: + clone "^2.1.1" + clone-buffer "^1.0.0" + clone-stats "^1.0.0" + cloneable-readable "^1.0.0" + remove-trailing-separator "^1.0.1" + replace-ext "^1.0.0" + +vscode-nls@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-4.1.1.tgz#f9916b64e4947b20322defb1e676a495861f133c" + integrity sha512-4R+2UoUUU/LdnMnFjePxfLqNhBS8lrAFyX7pjb2ud/lqDkrUavFUTcG7wR0HBZFakae0Q6KLBFjMS6W93F403A== + +vscode@1.1.26: + version "1.1.26" + resolved "https://registry.yarnpkg.com/vscode/-/vscode-1.1.26.tgz#33d0feacd8ab5f78a0c4672235376c70cdea494b" + integrity sha512-z1Nf5J38gjUFbuDCbJHPN6OJ//5EG+e/yHlh6ERxj/U9B2Qc3aiHaFr38/fee/GGnxvRw/XegLMOG+UJwKi/Qg== + dependencies: + glob "^7.1.2" + gulp-chmod "^2.0.0" + gulp-filter "^5.0.1" + gulp-gunzip "1.0.0" + gulp-remote-src-vscode "^0.5.1" + gulp-untar "^0.0.7" + gulp-vinyl-zip "^2.1.2" + mocha "^4.0.1" + request "^2.88.0" + semver "^5.4.1" + source-map-support "^0.5.0" + url-parse "^1.4.3" + vinyl-fs "^3.0.3" + vinyl-source-stream "^1.1.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + +xml@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/xml/-/xml-1.0.1.tgz#78ba72020029c5bc87b8a81a3cfcd74b4a2fc1e5" + integrity sha1-eLpyAgApxbyHuKgaPPzXS0ovweU= + +"xtend@>=4.0.0 <4.1.0-0", xtend@~4.0.0, xtend@~4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" + integrity sha1-pcbVMr5lbiPbgg77lDofBJmNY68= + +yauzl@^2.2.1: + version "2.10.0" + resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" + integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk= + dependencies: + buffer-crc32 "~0.2.3" + fd-slicer "~1.1.0" + +yazl@^2.2.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/yazl/-/yazl-2.5.1.tgz#a3d65d3dd659a5b0937850e8609f22fffa2b5c35" + integrity sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw== + dependencies: + buffer-crc32 "~0.2.3" diff --git a/extensions/notebook/src/jupyter/jupyterServerInstallation.ts b/extensions/notebook/src/jupyter/jupyterServerInstallation.ts index 3e799b0358..d88e158f9a 100644 --- a/extensions/notebook/src/jupyter/jupyterServerInstallation.ts +++ b/extensions/notebook/src/jupyter/jupyterServerInstallation.ts @@ -52,6 +52,8 @@ export interface IJupyterServerInstallation { installPipPackages(packages: PythonPkgDetails[], useMinVersion: boolean): Promise; uninstallPipPackages(packages: PythonPkgDetails[]): Promise; pythonExecutable: string; + pythonInstallationPath: string; + installPythonPackage(backgroundOperation: azdata.BackgroundOperation, usingExistingPython: boolean, pythonInstallationPath: string, outputChannel: OutputChannel): Promise; } export class JupyterServerInstallation implements IJupyterServerInstallation { public apiWrapper: ApiWrapper; @@ -128,7 +130,7 @@ export class JupyterServerInstallation implements IJupyterServerInstallation { backgroundOperation.updateStatus(azdata.TaskStatus.InProgress, msgInstallPkgProgress); try { - await this.installPythonPackage(backgroundOperation); + await this.installPythonPackage(backgroundOperation, this._usingExistingPython, this._pythonInstallationPath, this.outputChannel); if (this._usingExistingPython) { await this.upgradePythonPackages(false, forceInstall); @@ -146,8 +148,8 @@ export class JupyterServerInstallation implements IJupyterServerInstallation { } } - private installPythonPackage(backgroundOperation: azdata.BackgroundOperation): Promise { - if (this._usingExistingPython) { + public installPythonPackage(backgroundOperation: azdata.BackgroundOperation, usingExistingPython: boolean, pythonInstallationPath: string, outputChannel: OutputChannel): Promise { + if (usingExistingPython) { return Promise.resolve(); } @@ -174,7 +176,7 @@ export class JupyterServerInstallation implements IJupyterServerInstallation { } return new Promise((resolve, reject) => { - let installPath = this._pythonInstallationPath; + let installPath = pythonInstallationPath; backgroundOperation.updateStatus(azdata.TaskStatus.InProgress, msgDownloadPython(platformId, pythonDownloadUrl)); fs.mkdirs(installPath, (err) => { if (err) { @@ -198,7 +200,7 @@ export class JupyterServerInstallation implements IJupyterServerInstallation { let totalBytes = parseInt(response.headers['content-length']); totalMegaBytes = totalBytes / (1024 * 1024); - this.outputChannel.appendLine(`${msgPythonDownloadPending} (0 / ${totalMegaBytes.toFixed(2)} MB)`); + outputChannel.appendLine(`${msgPythonDownloadPending} (0 / ${totalMegaBytes.toFixed(2)} MB)`); }) .on('data', (data) => { receivedBytes += data.length; @@ -206,7 +208,7 @@ export class JupyterServerInstallation implements IJupyterServerInstallation { let receivedMegaBytes = receivedBytes / (1024 * 1024); let percentage = receivedMegaBytes / totalMegaBytes; if (percentage >= printThreshold) { - this.outputChannel.appendLine(`${msgPythonDownloadPending} (${receivedMegaBytes.toFixed(2)} / ${totalMegaBytes.toFixed(2)} MB)`); + outputChannel.appendLine(`${msgPythonDownloadPending} (${receivedMegaBytes.toFixed(2)} / ${totalMegaBytes.toFixed(2)} MB)`); printThreshold += 0.1; } } @@ -216,7 +218,7 @@ export class JupyterServerInstallation implements IJupyterServerInstallation { downloadRequest.pipe(fs.createWriteStream(pythonPackagePathLocal)) .on('close', async () => { //unpack python zip/tar file - this.outputChannel.appendLine(msgPythonUnpackPending); + outputChannel.appendLine(msgPythonUnpackPending); let pythonSourcePath = path.join(installPath, constants.pythonBundleVersion); if (await utils.exists(pythonSourcePath)) { try { @@ -235,7 +237,7 @@ export class JupyterServerInstallation implements IJupyterServerInstallation { } }); - this.outputChannel.appendLine(msgPythonDownloadComplete); + outputChannel.appendLine(msgPythonDownloadComplete); backgroundOperation.updateStatus(azdata.TaskStatus.InProgress, msgPythonDownloadComplete); resolve(); }).catch(err => { @@ -657,6 +659,13 @@ export class JupyterServerInstallation implements IJupyterServerInstallation { process.platform === constants.winPlatform ? 'Scripts\\conda.exe' : 'bin/conda'); } + /** + * Returns Python installation path + */ + public get pythonInstallationPath(): string { + return this._pythonInstallationPath; + } + public get usingConda(): boolean { return this._usingConda; } diff --git a/extensions/notebook/src/test/managePackages/localPackageManageProvider.test.ts b/extensions/notebook/src/test/managePackages/localPackageManageProvider.test.ts index 0a719fb905..0e38a4583e 100644 --- a/extensions/notebook/src/test/managePackages/localPackageManageProvider.test.ts +++ b/extensions/notebook/src/test/managePackages/localPackageManageProvider.test.ts @@ -5,6 +5,8 @@ 'use strict'; +import * as vscode from 'vscode'; +import * as azdata from 'azdata'; import * as should from 'should'; import 'mocha'; import * as TypeMoq from 'typemoq'; @@ -200,7 +202,9 @@ describe('Manage Package Providers', () => { executeStreamedCommand: (command: string) => { return Promise.resolve(); }, getCondaExePath: () => { return ''; }, pythonExecutable: '', - usingConda: false + pythonInstallationPath: '', + usingConda: false, + installPythonPackage: (backgroundOperation: azdata.BackgroundOperation, usingExistingPython: boolean, pythonInstallationPath: string, outputChannel: vscode.OutputChannel) => {return Promise.resolve(); } }, piPyClient: { fetchPypiPackage: (packageName) => { return Promise.resolve(); } diff --git a/scripts/test-extensions-unit.bat b/scripts/test-extensions-unit.bat index ab2d43897b..76cee63bc4 100755 --- a/scripts/test-extensions-unit.bat +++ b/scripts/test-extensions-unit.bat @@ -78,6 +78,11 @@ echo *** starting resource deployment tests *** echo ****************************************** call "%INTEGRATION_TEST_ELECTRON_PATH%" --nogpu --extensionDevelopmentPath=%~dp0\..\extensions\resource-deployment --extensionTestsPath=%~dp0\..\extensions\resource-deployment\out\test --user-data-dir=%VSCODEUSERDATADIR% --extensions-dir=%VSCODEEXTENSIONSDIR% --remote-debugging-port=9222 +echo ******************************* +echo *** starting machine-learning-services tests *** +echo ******************************* +call .\scripts\code.bat --extensionDevelopmentPath=%~dp0\..\extensions\machine-learning-services --extensionTestsPath=%~dp0\..\extensions\machine-learning-services\out\test --user-data-dir=%VSCODEUSERDATADIR% --extensions-dir=%VSCODEEXTENSIONSDIR% --remote-debugging-port=9222 + if %errorlevel% neq 0 exit /b %errorlevel% rmdir /s /q %VSCODEUSERDATADIR% diff --git a/scripts/test-extensions-unit.sh b/scripts/test-extensions-unit.sh index 8f192ded38..53df124c2a 100755 --- a/scripts/test-extensions-unit.sh +++ b/scripts/test-extensions-unit.sh @@ -79,5 +79,10 @@ echo *** starting resource deployment tests *** echo ****************************************** "$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX --nogpu --extensionDevelopmentPath=$ROOT/extensions/resource-deployment --extensionTestsPath=$ROOT/extensions/resource-deployment/out/test --user-data-dir=$VSCODEUSERDATADIR --extensions-dir=$VSCODEEXTDIR +echo ****************************************** +echo *** starting machine-learning-services tests *** +echo ****************************************** +"$INTEGRATION_TEST_ELECTRON_PATH" $LINUX_NO_SANDBOX --nogpu --extensionDevelopmentPath=$ROOT/extensions/machine-learning-services --extensionTestsPath=$ROOT/extensions/machine-learning-services/out/test --user-data-dir=$VSCODEUSERDATADIR --extensions-dir=$VSCODEEXTDIR + rm -r $VSCODEUSERDATADIR rm -r $VSCODEEXTDIR